@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.
Files changed (102) hide show
  1. package/dist/build/buildAgents.js +61 -16
  2. package/dist/build/buildApi/buildApi.js +4 -1
  3. package/dist/build/buildApi/buildEndpoint.js +8 -2
  4. package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
  5. package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
  6. package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
  7. package/dist/build/buildApi/resolveEndpointSchedules.js +40 -0
  8. package/dist/build/buildApi/validateEndpoint.js +96 -1
  9. package/dist/build/buildApp.js +13 -20
  10. package/dist/build/buildAppMeta.js +138 -0
  11. package/dist/build/buildAuth/buildAgentAuth.js +14 -1
  12. package/dist/build/buildAuth/buildAuth.js +8 -3
  13. package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
  14. package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
  15. package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
  16. package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
  17. package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
  18. package/dist/build/buildAuth/validateAuthConfig.js +15 -3
  19. package/dist/build/buildEnvironments.js +261 -0
  20. package/dist/build/buildImports/buildImportsDev.js +15 -0
  21. package/dist/build/buildImports/buildImportsProd.js +4 -1
  22. package/dist/build/buildJs/generateClientJsModule.js +27 -0
  23. package/dist/build/buildJs/writeJs.js +3 -5
  24. package/dist/build/buildLogger.js +22 -6
  25. package/dist/build/buildMcp.js +73 -0
  26. package/dist/build/buildModuleDefs.js +90 -48
  27. package/dist/build/buildModules.js +61 -14
  28. package/dist/build/buildNotifications.js +98 -0
  29. package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
  30. package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
  31. package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
  32. package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
  33. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
  34. package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
  35. package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
  36. package/dist/build/buildPages/buildPage.js +27 -3
  37. package/dist/build/buildPages/buildSubscriptions.js +145 -0
  38. package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
  39. package/dist/build/buildPages/validateStateReferences.js +4 -2
  40. package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
  41. package/dist/build/buildRefs/buildRefs.js +21 -12
  42. package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
  43. package/dist/build/buildRefs/deferredRegions.js +80 -0
  44. package/dist/build/buildRefs/deferredRegistry.js +292 -0
  45. package/dist/build/buildRefs/getModuleRefContent.js +21 -6
  46. package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
  47. package/dist/build/buildRefs/parseRefContent.js +24 -0
  48. package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
  49. package/dist/build/buildRefs/walker.js +471 -207
  50. package/dist/build/buildTypes.js +22 -1
  51. package/dist/build/buildWebsockets.js +99 -0
  52. package/dist/build/collectPageContent.js +7 -1
  53. package/dist/build/copyAgentFileSystems.js +2 -2
  54. package/dist/build/full/buildPages.js +16 -0
  55. package/dist/build/full/updateServerPackageJson.js +21 -0
  56. package/dist/build/jit/buildPageJit.js +46 -6
  57. package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
  58. package/dist/build/jit/pageContentKeys.js +1 -0
  59. package/dist/build/jit/shallowBuild.js +84 -3
  60. package/dist/build/jit/writePageJit.js +11 -8
  61. package/dist/build/registerModules.js +125 -24
  62. package/dist/build/validateCallAgentSteps.js +58 -0
  63. package/dist/build/validateConfig.js +6 -1
  64. package/dist/build/validateEnvironmentGuards.js +69 -0
  65. package/dist/build/validateRenderNotificationSteps.js +50 -0
  66. package/dist/build/writeApi.js +98 -1
  67. package/dist/build/writeMcp.js +21 -0
  68. package/dist/build/writeNotifications.js +30 -0
  69. package/dist/build/writePluginImports/importPluginModule.js +39 -0
  70. package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
  71. package/dist/build/writePluginImports/writeAuthImports.js +4 -0
  72. package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
  73. package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
  74. package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
  75. package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
  76. package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
  77. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
  78. package/dist/build/writePluginImports/writePluginImports.js +18 -4
  79. package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
  80. package/dist/build/writeWebsockets.js +30 -0
  81. package/dist/createContext.js +6 -1
  82. package/dist/defaultPackages.js +7 -1
  83. package/dist/defaultTypesMap.js +688 -388
  84. package/dist/dynamic/buildDynamicBlocks.js +120 -0
  85. package/dist/index.js +81 -10
  86. package/dist/indexDev.js +4 -0
  87. package/dist/lowdefySchema.js +969 -6
  88. package/dist/scripts/generateDefaultTypes.js +5 -2
  89. package/dist/test-utils/expectTerminates.js +33 -0
  90. package/dist/test-utils/runBuild.js +15 -1
  91. package/dist/test-utils/runBuildForSnapshots.js +37 -4
  92. package/dist/test-utils/testContext.js +8 -2
  93. package/dist/utils/countOperators.js +5 -17
  94. package/dist/utils/createPluginTypesMap.js +21 -0
  95. package/dist/utils/getEnvironmentNames.js +26 -0
  96. package/dist/utils/logCollectedErrors.js +15 -9
  97. package/dist/utils/serializeBuildException.js +29 -0
  98. package/dist/utils/validateCronExpression.js +131 -0
  99. package/dist/utils/writeBuildArtifact.js +5 -2
  100. package/package.json +67 -60
  101. package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
  102. /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
@@ -20,9 +20,9 @@ import operators from '@lowdefy/operators-js/operators/build';
20
20
  import { resolve, WalkContext } from './buildRefs/walker.js';
21
21
  import getRefContent from './buildRefs/getRefContent.js';
22
22
  import makeRefDefinition from './buildRefs/makeRefDefinition.js';
23
- import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
24
23
  import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
25
24
  import validateOperatorsDynamic from './validateOperatorsDynamic.js';
25
+ import { makeShouldStop } from './buildRefs/deferredRegions.js';
26
26
  validateOperatorsDynamic({
27
27
  operators
28
28
  });
@@ -48,11 +48,69 @@ function validateRequiredVars(varDefs, consumerVars, entryId, source, prefix = '
48
48
  }
49
49
  }
50
50
  }
51
+ function getRuntimeOperatorKey(value) {
52
+ if (!type.isObject(value)) return null;
53
+ const nonTildeKeys = Object.keys(value).filter((k)=>!k.startsWith('~'));
54
+ if (nonTildeKeys.length === 1 && nonTildeKeys[0].startsWith('_')) return nonTildeKeys[0];
55
+ return null;
56
+ }
57
+ function suggestBuildOperator(operatorKey) {
58
+ // '_string.concat' -> '_build.string.concat', '_sum' -> '_build.sum'
59
+ return `_build.${operatorKey.slice(1)}`;
60
+ }
61
+ // Component bodies are preserved by config path (components.<i>.component), so
62
+ // bodies under an operator object at `components` itself or at an array element
63
+ // escape preservation and resolve eagerly during the manifest walk. That is
64
+ // harmless for static bodies (operator-composed component lists are supported —
65
+ // pinned by fixture 81-cross-module-build-op-components), but a _var in such a
66
+ // body resolves against the manifest walk's scope instead of the consumer's
67
+ // per-ref vars — a silently wrong value. Error on that combination explicitly.
68
+ // A _ref at either position is fully safe: it resolves top-down, so paths
69
+ // inside the ref'd file still match the preserve regex. An operator at
70
+ // components.<i>.component itself is inside the preserved body and is fine.
71
+ function findVarNode(value) {
72
+ if (type.isArray(value)) {
73
+ for (const item of value){
74
+ const found = findVarNode(item);
75
+ if (found) return found;
76
+ }
77
+ return null;
78
+ }
79
+ if (!type.isObject(value)) return null;
80
+ if (getRuntimeOperatorKey(value) === '_var') return value;
81
+ for (const key of Object.keys(value)){
82
+ const found = findVarNode(value[key]);
83
+ if (found) return found;
84
+ }
85
+ return null;
86
+ }
87
+ function assertStaticComponentsList({ components, entryId, filePath }) {
88
+ if (!type.isObject(components) && !type.isArray(components)) return;
89
+ const check = (value, position)=>{
90
+ const operatorKey = getRuntimeOperatorKey(value);
91
+ if (operatorKey && operatorKey !== '_ref' && findVarNode(value)) {
92
+ throw new ConfigError(`Module "${entryId}": _var inside an operator-generated components section ` + `cannot resolve per consumer. Found "${operatorKey}" at ${position} with a _var ` + `in its content. Define components whose bodies use _var as static ` + `{ id, component } list items so the bodies are preserved.`, {
93
+ filePath
94
+ });
95
+ }
96
+ };
97
+ check(components, '"components"');
98
+ if (type.isArray(components)) {
99
+ components.forEach((item, i)=>check(item, `"components.${i}"`));
100
+ }
101
+ }
51
102
  function validateVarTypes(varDefs, resolvedVarCache, entryId, source, prefix = '') {
52
103
  for (const [varName, varDef] of Object.entries(varDefs)){
53
104
  const fullName = prefix ? `${prefix}.${varName}` : varName;
54
105
  const value = resolvedVarCache[fullName];
55
106
  if (varDef.type && !type.isNone(value)) {
107
+ // A typed var must hold a concrete value — runtime operators are not allowed
108
+ // regardless of whether they are static-foldable or dynamic. Suggest the
109
+ // build-time equivalent so the user knows how to fix it.
110
+ const operatorKey = getRuntimeOperatorKey(value);
111
+ if (operatorKey) {
112
+ throw new ConfigError(`Module "${entryId}" (${source}) var "${fullName}" is typed "${varDef.type}" ` + `but received a runtime operator "${operatorKey}". ` + `Use the build-time equivalent "${suggestBuildOperator(operatorKey)}" instead.` + (varDef.description ? `\n - ${varDef.description}` : ''));
113
+ }
56
114
  if (type.typeOf(value) !== varDef.type) {
57
115
  throw new ConfigError(`Module "${entryId}" (${source}) var "${fullName}" must be type ` + `"${varDef.type}" but got "${type.typeOf(value)}".` + (varDef.description ? `\n - ${varDef.description}` : ''));
58
116
  }
@@ -85,7 +143,17 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
85
143
  refDef,
86
144
  referencedFrom: null
87
145
  });
88
- // Run walker with shouldStop preserving content that may contain cross-module refs
146
+ if (type.isObject(content)) {
147
+ assertStaticComponentsList({
148
+ components: content.components,
149
+ entryId: entry.id,
150
+ filePath: moduleYamlPath
151
+ });
152
+ }
153
+ // Phase A — header parse: walk only the static header keys (dependencies,
154
+ // plugins, secrets, vars definitions — defaults record-ified). Content
155
+ // sections stay raw parsed YAML; Phase C.5 record-ifies exportables and
156
+ // Phase D resolves the rest.
89
157
  const ctx = new WalkContext({
90
158
  buildContext: context,
91
159
  refId: refDef.id,
@@ -93,6 +161,9 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
93
161
  vars: {},
94
162
  moduleRoot,
95
163
  packageRoot,
164
+ // The entry object is created after this walk; deferred-record coordinates
165
+ // still need the owning entry id.
166
+ entryId: entry.id,
96
167
  path: '',
97
168
  currentFile: moduleYamlPath,
98
169
  refChain: new Set(refDef.path ? [
@@ -100,16 +171,9 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
100
171
  ] : []),
101
172
  operators,
102
173
  env: process.env,
174
+ lowdefyApp: context.appMeta,
103
175
  dynamicIdentifiers,
104
- shouldStop: (childPath)=>{
105
- if (/^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/.test(childPath)) return 'preserve';
106
- if (/^components\.\d+\.component$/.test(childPath)) return 'preserve';
107
- if (/^pages(\..*)?$/.test(childPath)) return 'preserve';
108
- if (/^api(\..*)?$/.test(childPath)) return 'preserve';
109
- if (/^connections(\..*)?$/.test(childPath)) return 'preserve';
110
- if (/^menus\.\d+\.links$/.test(childPath)) return 'preserve';
111
- return false;
112
- }
176
+ shouldStop: makeShouldStop('header')
113
177
  });
114
178
  const manifest = await resolve(content, ctx);
115
179
  // Parse dependencies array from manifest
@@ -173,6 +237,49 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
173
237
  throw error;
174
238
  }
175
239
  }
240
+ // Phase C.5 — record-ify exportables: walk only the components and menus
241
+ // sections in module-static scope (entry id known, but NO moduleEntry on the
242
+ // context — a _module.var at a structural position errors, because export ids
243
+ // must not vary per consumer). File refs are entered and static operators
244
+ // resolve; component bodies and menu links become records before any phase can
245
+ // consume them, so cross-entry consumption never races record creation.
246
+ async function recordifyExportables({ entryId, context }) {
247
+ const moduleEntry = context.modules[entryId];
248
+ const { manifest, packageRoot, moduleRoot, refDef } = moduleEntry;
249
+ const moduleYamlPath = path.join(moduleRoot, 'module.lowdefy.yaml');
250
+ if (type.isObject(manifest)) {
251
+ assertStaticComponentsList({
252
+ components: manifest.components,
253
+ entryId,
254
+ filePath: moduleYamlPath
255
+ });
256
+ }
257
+ const ctx = new WalkContext({
258
+ buildContext: context,
259
+ refId: refDef.id,
260
+ sourceRefId: null,
261
+ vars: {},
262
+ moduleRoot,
263
+ packageRoot,
264
+ entryId,
265
+ path: '',
266
+ currentFile: moduleYamlPath,
267
+ refChain: new Set(refDef.path ? [
268
+ refDef.path
269
+ ] : []),
270
+ operators,
271
+ env: process.env,
272
+ lowdefyApp: context.appMeta,
273
+ dynamicIdentifiers,
274
+ shouldStop: makeShouldStop('exportables')
275
+ });
276
+ moduleEntry.manifest = await resolve(manifest, ctx);
277
+ }
278
+ // Phase D — manifest resolve: ONE full walk with the module entry in context.
279
+ // Pages, api, and connections resolve completely; component/menuLinks
280
+ // placeholders pass through the kind-aware dispatch untouched; varDefault
281
+ // placeholders keep their rule so the dispatch cannot force demand-only
282
+ // records.
176
283
  async function resolveFullManifest({ entryId, context }) {
177
284
  const moduleEntry = context.modules[entryId];
178
285
  const { manifest, packageRoot, moduleRoot, moduleDependencies, refDef } = moduleEntry;
@@ -193,24 +300,18 @@ async function resolveFullManifest({ entryId, context }) {
193
300
  ] : []),
194
301
  operators,
195
302
  env: process.env,
303
+ lowdefyApp: context.appMeta,
196
304
  dynamicIdentifiers,
197
- shouldStop: (childPath)=>{
198
- if (/^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/.test(childPath)) return 'preserve';
199
- if (/^components\.\d+\.component$/.test(childPath)) return 'preserve';
200
- return false;
201
- }
202
- });
203
- let resolved = await resolve(manifest, ctx);
204
- resolved = evaluateStaticOperators({
205
- context,
206
- input: resolved,
207
- refDef
305
+ shouldStop: makeShouldStop('manifest')
208
306
  });
307
+ const resolved = await resolve(manifest, ctx);
209
308
  // Filter null entries produced by _ref resolution failures
210
309
  for (const key of [
211
310
  'pages',
212
311
  'connections',
213
- 'api'
312
+ 'api',
313
+ 'agents',
314
+ 'notifications'
214
315
  ]){
215
316
  if (type.isArray(resolved[key])) {
216
317
  resolved[key] = resolved[key].filter((item)=>!type.isNone(item));
@@ -223,4 +324,4 @@ async function resolveFullManifest({ entryId, context }) {
223
324
  validateVarTypes(varDefs, moduleEntry.resolvedVarCache, entryId, moduleEntry.source);
224
325
  }
225
326
  }
226
- export { resolveLocalManifest, resolveFullManifest, validateRequiredVars };
327
+ export { resolveLocalManifest, recordifyExportables, resolveFullManifest, validateRequiredVars };
@@ -0,0 +1,58 @@
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
+ function collectCallAgentSteps(routine, steps) {
18
+ if (type.isArray(routine)) {
19
+ routine.forEach((item)=>collectCallAgentSteps(item, steps));
20
+ return;
21
+ }
22
+ if (type.isObject(routine)) {
23
+ if (routine.type === 'CallAgent') {
24
+ steps.push(routine);
25
+ }
26
+ // Recurse into all values (handles control structures like :then, :else, :try, :catch)
27
+ Object.values(routine).forEach((value)=>collectCallAgentSteps(value, steps));
28
+ }
29
+ }
30
+ // Runs after buildAgents (needs context.agentIds and normalized agent.tools).
31
+ // Validates that CallAgent steps with a static agentId reference an existing
32
+ // agent, and warns when that agent has confirm tools — headless runs have no
33
+ // client to resolve an approval, so those tools auto-execute. Operator
34
+ // (dynamic) agentIds are skipped; they resolve at runtime.
35
+ function validateCallAgentSteps({ components, context }) {
36
+ (components.api ?? []).forEach((endpoint)=>{
37
+ const steps = [];
38
+ collectCallAgentSteps(endpoint.routine, steps);
39
+ steps.forEach((step)=>{
40
+ const agentId = step.properties?.agentId;
41
+ if (!type.isString(agentId)) return;
42
+ if (!context.agentIds?.has(agentId)) {
43
+ throw new ConfigError(`CallAgent step "${step.stepId}" at endpoint "${endpoint.endpointId}" references agent "${agentId}" which does not exist.`, {
44
+ configKey: step['~k']
45
+ });
46
+ }
47
+ const agent = (components.agents ?? []).find((a)=>a.agentId === agentId);
48
+ const hasConfirmTools = (agent?.tools ?? []).some((t)=>t.confirm);
49
+ if (hasConfirmTools) {
50
+ context.handleWarning(new ConfigWarning(`Agent "${agentId}" has tools with confirm: true, but tool approval is not supported when run from a CallAgent step. Tools will auto-execute.`, {
51
+ configKey: step['~k']
52
+ }));
53
+ }
54
+ });
55
+ });
56
+ return components;
57
+ }
58
+ export default validateCallAgentSteps;
@@ -13,7 +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
- function validateConfig({ components }) {
16
+ import buildEnvironments from './buildEnvironments.js';
17
+ function validateConfig({ components, context }) {
17
18
  if (type.isNone(components.config)) {
18
19
  components.config = {};
19
20
  }
@@ -25,6 +26,10 @@ function validateConfig({ components }) {
25
26
  throw new Error('Base path must start with "/".');
26
27
  }
27
28
  }
29
+ buildEnvironments({
30
+ components,
31
+ context
32
+ });
28
33
  return components;
29
34
  }
30
35
  export default validateConfig;
@@ -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;
@@ -0,0 +1,50 @@
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
+ function collectRenderNotificationSteps(routine, steps) {
18
+ if (type.isArray(routine)) {
19
+ routine.forEach((item)=>collectRenderNotificationSteps(item, steps));
20
+ return;
21
+ }
22
+ if (type.isObject(routine)) {
23
+ if (routine.type === 'RenderNotification') {
24
+ steps.push(routine);
25
+ }
26
+ // Recurse into all values (handles control structures like :then, :else, :try, :catch)
27
+ Object.values(routine).forEach((value)=>collectRenderNotificationSteps(value, steps));
28
+ }
29
+ }
30
+ // Runs after buildNotifications (needs context.notificationIds). Validates that
31
+ // RenderNotification steps with a static notificationId reference an existing
32
+ // notification. Operator (dynamic) notificationIds are skipped; they resolve at
33
+ // runtime.
34
+ function validateRenderNotificationSteps({ components, context }) {
35
+ (components.api ?? []).forEach((endpoint)=>{
36
+ const steps = [];
37
+ collectRenderNotificationSteps(endpoint.routine, steps);
38
+ steps.forEach((step)=>{
39
+ const notificationId = step.properties?.notificationId;
40
+ if (!type.isString(notificationId)) return;
41
+ if (!context.notificationIds?.has(notificationId)) {
42
+ throw new ConfigError(`RenderNotification step "${step.stepId}" at endpoint "${endpoint.endpointId}" references notification "${notificationId}" which does not exist.`, {
43
+ configKey: step['~k']
44
+ });
45
+ }
46
+ });
47
+ });
48
+ return components;
49
+ }
50
+ export default validateRenderNotificationSteps;
@@ -13,9 +13,102 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { type, serializer } from '@lowdefy/helpers';
16
+ import getEnvironmentNames from '../utils/getEnvironmentNames.js';
16
17
  async function writeEndpoint({ endpoint, context }) {
17
18
  await context.writeBuildArtifact(`api/${endpoint.endpointId}.json`, serializer.serializeToString(endpoint ?? {}));
18
19
  }
20
+ // Flat manifest of every scheduled endpoint, consumed by the Vercel Build Output assembly to
21
+ // generate the `crons` array in config.json. The runtime reads schedules off the endpoint artifact
22
+ // directly, so it does not depend on this file.
23
+ //
24
+ // With config.environments declared there is one entry per registered environment and schedule:
25
+ // `environment` names it and `forward` is true for environments this deployment forwards to.
26
+ // Vercel fires crons only on production, so:
27
+ // - the current environment registers its own schedules;
28
+ // - an environment with a cron.secret is forwarded to, so its own crons never fire it; any other
29
+ // current environment is the one Vercel fires crons on and also registers every forwarded
30
+ // environment's schedules;
31
+ // - without a current environment only the `default` schedules are registered;
32
+ // - cron.enabled: false registers nothing for that environment.
33
+ function getSchedules({ endpoint, key }) {
34
+ if (type.isArray(endpoint.schedules)) return endpoint.schedules;
35
+ return endpoint.schedules?.[key] ?? [];
36
+ }
37
+ function toManifestEntries({ endpoint, key, environment, forward }) {
38
+ return getSchedules({
39
+ endpoint,
40
+ key
41
+ }).map((schedule)=>({
42
+ endpointId: endpoint.endpointId,
43
+ cron: schedule.cron,
44
+ payload: schedule.payload ?? {},
45
+ ...type.isUndefined(environment) ? {} : {
46
+ environment,
47
+ forward
48
+ }
49
+ }));
50
+ }
51
+ function isCronEnabled(environment) {
52
+ return environment.cron?.enabled !== false;
53
+ }
54
+ function isForwardTarget(environment) {
55
+ return !type.isUndefined(environment.cron?.secret);
56
+ }
57
+ function getRegisteredEnvironments({ environments, current }) {
58
+ const registered = [];
59
+ if (isCronEnabled(environments[current])) {
60
+ registered.push({
61
+ name: current,
62
+ forward: false
63
+ });
64
+ }
65
+ if (isForwardTarget(environments[current])) return registered;
66
+ getEnvironmentNames(environments).forEach((name)=>{
67
+ const environment = environments[name];
68
+ if (name === current || !isForwardTarget(environment) || !isCronEnabled(environment)) return;
69
+ registered.push({
70
+ name,
71
+ forward: true
72
+ });
73
+ });
74
+ return registered;
75
+ }
76
+ async function writeSchedulesManifest({ components, context }) {
77
+ const { environments, environment: current } = components.config ?? {};
78
+ const declared = getEnvironmentNames(environments).length > 0;
79
+ const registered = declared && !type.isUndefined(current) ? getRegisteredEnvironments({
80
+ environments,
81
+ current
82
+ }) : [];
83
+ const schedules = [];
84
+ (components.api ?? []).forEach((endpoint)=>{
85
+ if (!declared) {
86
+ schedules.push(...toManifestEntries({
87
+ endpoint
88
+ }));
89
+ return;
90
+ }
91
+ if (type.isUndefined(current)) {
92
+ schedules.push(...toManifestEntries({
93
+ endpoint,
94
+ key: 'default'
95
+ }));
96
+ return;
97
+ }
98
+ registered.forEach(({ name, forward })=>{
99
+ schedules.push(...toManifestEntries({
100
+ endpoint,
101
+ key: name,
102
+ environment: name,
103
+ forward
104
+ }));
105
+ });
106
+ });
107
+ // Only emit the manifest when something is scheduled; the Vercel assembly treats a missing
108
+ // schedules.json as "no crons".
109
+ if (schedules.length === 0) return;
110
+ await context.writeBuildArtifact('schedules.json', serializer.serializeToString(schedules));
111
+ }
19
112
  async function writeApi({ components, context }) {
20
113
  if (type.isNone(components.api)) return;
21
114
  if (!type.isArray(components.api)) {
@@ -25,6 +118,10 @@ async function writeApi({ components, context }) {
25
118
  endpoint,
26
119
  context
27
120
  }));
28
- return Promise.all(writePromises);
121
+ await Promise.all(writePromises);
122
+ await writeSchedulesManifest({
123
+ components,
124
+ context
125
+ });
29
126
  }
30
127
  export default writeApi;
@@ -0,0 +1,21 @@
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 { serializer } from '@lowdefy/helpers';
16
+ // Always written, even unconfigured - the runtime reads mcp.configured and
17
+ // never needs an existence check on the artifact.
18
+ async function writeMcp({ components, context }) {
19
+ await context.writeBuildArtifact('mcp.json', serializer.serializeToString(components.mcp));
20
+ }
21
+ export default writeMcp;
@@ -0,0 +1,30 @@
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, serializer } from '@lowdefy/helpers';
16
+ async function writeNotification({ notification, context }) {
17
+ await context.writeBuildArtifact(`notifications/${notification.notificationId}.json`, serializer.serializeToString(notification ?? {}));
18
+ }
19
+ async function writeNotifications({ components, context }) {
20
+ if (type.isNone(components.notifications)) return;
21
+ if (!type.isArray(components.notifications)) {
22
+ throw new Error(`Notifications is not an array.`);
23
+ }
24
+ const writePromises = components.notifications.map((notification)=>writeNotification({
25
+ notification,
26
+ context
27
+ }));
28
+ return Promise.all(writePromises);
29
+ }
30
+ export default writeNotifications;
@@ -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 { createRequire } from 'node:module';
16
+ import path from 'node:path';
17
+ // Import a plugin module (e.g. `${pkg}/schemas`, `${pkg}/connections`) for
18
+ // schema collection. Default packages resolve from the build package itself;
19
+ // custom plugins only exist in the server's node_modules, so fall back to
20
+ // resolving from the server directory. Returns undefined when unresolvable —
21
+ // callers degrade gracefully.
22
+ async function importPluginModule({ context, specifier }) {
23
+ try {
24
+ return await import(/* webpackIgnore: true */ /* @vite-ignore */ specifier);
25
+ } catch {
26
+ // Not resolvable from the build package — try the server's node_modules.
27
+ }
28
+ const serverDir = context.directories?.server;
29
+ if (!serverDir) {
30
+ return undefined;
31
+ }
32
+ try {
33
+ const require = createRequire(path.join(serverDir, 'package.json'));
34
+ return await import(/* webpackIgnore: true */ /* @vite-ignore */ require.resolve(specifier));
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ export default importPluginModule;
@@ -12,7 +12,8 @@
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
- */ async function writeActionSchemaMap({ components, context }) {
15
+ */ import importPluginModule from './importPluginModule.js';
16
+ async function writeActionSchemaMap({ components, context }) {
16
17
  const schemas = {};
17
18
  const typesMapSchemas = context.typesMap.schemas?.actions ?? {};
18
19
  const actionsByPackage = {};
@@ -23,12 +24,10 @@
23
24
  actionsByPackage[action.package].push(action);
24
25
  }
25
26
  for (const [packageName, actions] of Object.entries(actionsByPackage)){
26
- let packageSchemas;
27
- try {
28
- packageSchemas = await import(/* webpackIgnore: true */ `${packageName}/schemas`);
29
- } catch {
30
- // Package not resolvable from build context (custom plugins) — skip
31
- }
27
+ const packageSchemas = await importPluginModule({
28
+ context,
29
+ specifier: `${packageName}/schemas`
30
+ });
32
31
  for (const action of actions){
33
32
  if (typesMapSchemas[action.typeName]) {
34
33
  schemas[action.typeName] = typesMapSchemas[action.typeName];
@@ -30,5 +30,9 @@ async function writeAuthImports({ components, context }) {
30
30
  imports: components.imports.auth.providers,
31
31
  importPath: 'auth/providers'
32
32
  }));
33
+ await context.writeBuildArtifact('plugins/auth/strategies.js', generateImportFile({
34
+ imports: components.imports.auth.strategies,
35
+ importPath: 'auth/strategies'
36
+ }));
33
37
  }
34
38
  export default writeAuthImports;