@bsm-form/core 0.39.0 → 0.41.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/index.cjs CHANGED
@@ -27,24 +27,236 @@ __export(index_exports, {
27
27
  adaptDisplay: () => adaptDisplay,
28
28
  adaptField: () => adaptField,
29
29
  adaptLayout: () => adaptLayout,
30
+ collectFormEnvReferences: () => collectFormEnvReferences,
30
31
  compileSchema: () => compileSchema,
31
32
  conditions: () => conditions,
32
33
  createFormEngineEnvironment: () => createFormEngineEnvironment,
33
34
  createRuntimeNode: () => createRuntimeNode,
34
35
  formatTagSourceValue: () => formatTagSourceValue,
35
36
  isThenable: () => isThenable,
37
+ remapFormEnvReferences: () => remapFormEnvReferences,
38
+ resolveFormEnv: () => resolveFormEnv,
36
39
  resolveLoadingProp: () => resolveLoadingProp,
37
40
  resolveTagValueMapping: () => resolveTagValueMapping,
38
41
  runConditionEngine: () => runConditionEngine,
39
42
  tagMapKey: () => tagMapKey,
40
43
  traverseNode: () => traverseNode,
44
+ validateApiUrlTemplate: () => validateApiUrlTemplate,
45
+ validateEnvBaseUrl: () => validateEnvBaseUrl,
46
+ validateEnvDefinitions: () => validateEnvDefinitions,
41
47
  validateField: () => validateField,
48
+ validateFormEnvReferences: () => validateFormEnvReferences,
42
49
  validateSchema: () => validateSchema,
43
50
  validation: () => validation,
44
51
  validationRegistry: () => validationRegistry
45
52
  });
46
53
  module.exports = __toCommonJS(index_exports);
47
54
 
55
+ // src/utils/form-env.ts
56
+ function validateEnvBaseUrl(value) {
57
+ if (!value || /[\\\s?#]/.test(value) || value.startsWith("//")) throw new Error("Base URL cannot contain whitespace, query, fragment, or backslashes");
58
+ if (value.startsWith("/")) return;
59
+ let url;
60
+ try {
61
+ url = new URL(value);
62
+ } catch {
63
+ throw new Error("Expected an http(s) URL or path starting with /");
64
+ }
65
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error("Base URL must use http(s) without credentials");
66
+ }
67
+ function validateValue(name, type, value) {
68
+ if (typeof value !== (type === "url" ? "string" : type) || typeof value === "number" && !Number.isFinite(value)) throw new Error(`env.${name}: expected ${String(type)}`);
69
+ if (type === "url") {
70
+ try {
71
+ validateEnvBaseUrl(value);
72
+ } catch (error) {
73
+ throw new Error(`env.${name}: ${error instanceof Error ? error.message : String(error)}`);
74
+ }
75
+ }
76
+ }
77
+ function validateEnvDefinitions(value) {
78
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Environment definitions must be an object");
79
+ for (const [name, entry] of Object.entries(value)) {
80
+ if (!/^[A-Za-z_]+$/.test(name) || ["__proto__", "prototype", "constructor"].includes(name)) throw new Error(`Invalid environment variable name: ${name}`);
81
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`env.${name}: expected a definition`);
82
+ const definition = entry;
83
+ if (!["string", "number", "boolean", "url"].includes(String(definition.type))) throw new Error(`env.${name}: invalid type`);
84
+ for (const key of Object.keys(definition)) if (!["type", "label", "description", "default", "required", "allowOverride"].includes(key)) throw new Error(`env.${name}: unknown property ${key}`);
85
+ for (const key of ["required", "allowOverride"]) if (definition[key] !== void 0 && typeof definition[key] !== "boolean") throw new Error(`env.${name}.${key}: expected boolean`);
86
+ for (const key of ["label", "description"]) if (definition[key] !== void 0 && typeof definition[key] !== "string") throw new Error(`env.${name}.${key}: expected string`);
87
+ if (Object.hasOwn(definition, "default")) validateValue(name, definition.type, definition.default);
88
+ }
89
+ }
90
+ function resolveFormEnv(config = {}, requireValues = true) {
91
+ const definitions = config.definitions ?? {};
92
+ const overrides = config.overrides ?? {};
93
+ validateEnvDefinitions(definitions);
94
+ if (typeof overrides !== "object" || Array.isArray(overrides)) throw new Error("Environment overrides must be an object");
95
+ for (const name of Object.keys(overrides)) {
96
+ if (!Object.hasOwn(definitions, name)) throw new Error(`env.${name}: unknown override`);
97
+ if (definitions[name]?.allowOverride === false) throw new Error(`env.${name}: overrides are disabled`);
98
+ }
99
+ const values = {};
100
+ for (const [name, definition] of Object.entries(definitions)) {
101
+ const provided = Object.hasOwn(overrides, name);
102
+ const value = provided ? overrides[name] : definition.default;
103
+ if (provided || value !== void 0) validateValue(name, definition.type, value);
104
+ if (requireValues && definition.required && (value === void 0 || value === "")) throw new Error(`env.${name}: required value is missing`);
105
+ if (value !== void 0) values[name] = value;
106
+ }
107
+ return Object.freeze(values);
108
+ }
109
+
110
+ // src/utils/get-path.ts
111
+ function pathSegments(path) {
112
+ const segments = [];
113
+ let i = 0;
114
+ while (i < path.length) {
115
+ const char = path[i];
116
+ if (char === ".") {
117
+ i += 1;
118
+ continue;
119
+ }
120
+ if (char === "[") {
121
+ const close = path.indexOf("]", i);
122
+ if (close === -1) {
123
+ segments.push(path.slice(i));
124
+ break;
125
+ }
126
+ segments.push(path.slice(i + 1, close));
127
+ i = close + 1;
128
+ continue;
129
+ }
130
+ let end = i;
131
+ while (end < path.length && path[end] !== "." && path[end] !== "[") {
132
+ end += 1;
133
+ }
134
+ if (end > i) {
135
+ segments.push(path.slice(i, end));
136
+ }
137
+ i = end;
138
+ }
139
+ return segments;
140
+ }
141
+ function getPath(obj, path) {
142
+ if (!path) {
143
+ return obj;
144
+ }
145
+ let current = obj;
146
+ for (const key of pathSegments(path)) {
147
+ if (typeof current !== "object" || current === null) {
148
+ return void 0;
149
+ }
150
+ current = current[key];
151
+ }
152
+ return current;
153
+ }
154
+
155
+ // src/utils/scope-path.ts
156
+ function resolveScopedPath(ctx, path) {
157
+ if (path === "env") return ctx.env ?? {};
158
+ if (path.startsWith("env.")) return getPath(ctx.env ?? {}, path.slice(4));
159
+ if (path === "$values" || path === "values") {
160
+ return ctx.values;
161
+ }
162
+ if (path === "$resources" || path === "resources") {
163
+ return ctx.resources;
164
+ }
165
+ if (path === "$row" || path === "row") {
166
+ return ctx.row;
167
+ }
168
+ if (path === "$item" || path === "item") {
169
+ return ctx.item;
170
+ }
171
+ if (path.startsWith("values.")) {
172
+ return getPath(ctx.values, path.replace("values.", ""));
173
+ }
174
+ if (path.startsWith("resources.")) {
175
+ return getPath(ctx.resources, path.replace("resources.", ""));
176
+ }
177
+ if (path.startsWith("row.")) {
178
+ return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
179
+ }
180
+ if (path.startsWith("item.")) {
181
+ return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
182
+ }
183
+ return getPath(ctx.values, path);
184
+ }
185
+ function isExplicitScopedPath(path) {
186
+ return path === "env" || path.startsWith("env.") || path === "$values" || path === "values" || path === "$resources" || path === "resources" || path === "$row" || path === "row" || path === "$item" || path === "item" || path.startsWith("values.") || path.startsWith("resources.") || path.startsWith("row.") || path.startsWith("item.");
187
+ }
188
+ function resolvePathStringsInData(value, ctx) {
189
+ if (typeof value === "string") {
190
+ return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
191
+ }
192
+ if (Array.isArray(value)) {
193
+ return value.map((item) => resolvePathStringsInData(item, ctx));
194
+ }
195
+ if (typeof value === "object" && value !== null) {
196
+ const result = {};
197
+ for (const [key, entry] of Object.entries(value)) {
198
+ result[key] = resolvePathStringsInData(entry, ctx);
199
+ }
200
+ return result;
201
+ }
202
+ return value;
203
+ }
204
+
205
+ // src/utils/url-template.ts
206
+ var pathPattern = /^(values|resources|env)\.[A-Za-z_$][\w$]*(?:(?:\.(?:[A-Za-z_$][\w$]*|\d+))|(?:\[\d+\]))*$/;
207
+ function placeholders(url) {
208
+ const result = [];
209
+ let cursor = 0;
210
+ while (true) {
211
+ const start = url.indexOf("${", cursor);
212
+ if (start < 0) break;
213
+ const close = url.indexOf("}", start + 2);
214
+ if (close < 0) throw new Error("API URL template has an unclosed placeholder");
215
+ const path = url.slice(start + 2, close).trim();
216
+ if (!pathPattern.test(path)) {
217
+ throw new Error(`Invalid API URL placeholder: ${path}. Use a values.*, resources.*, or env.* data path`);
218
+ }
219
+ const origin = url.match(/^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/[^/?#]*/)?.[0];
220
+ if (start === 0 && !path.startsWith("env.") || origin && start < origin.length) {
221
+ throw new Error("API URL placeholders cannot replace the service origin; use a static base URL");
222
+ }
223
+ result.push({ start, end: close + 1, path });
224
+ cursor = close + 1;
225
+ }
226
+ return result;
227
+ }
228
+ function validateApiUrlTemplate(url) {
229
+ if (!url.trim()) throw new Error("API URL must not be empty");
230
+ placeholders(url);
231
+ }
232
+ function resolveApiUrlTemplate(url, context, definitions = {}) {
233
+ validateApiUrlTemplate(url);
234
+ let result = "";
235
+ let cursor = 0;
236
+ for (const placeholder of placeholders(url)) {
237
+ const value = resolveScopedPath(context, placeholder.path);
238
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" || typeof value === "number" && !Number.isFinite(value) || value === "") {
239
+ throw new Error(`API URL placeholder ${placeholder.path} must resolve to a non-empty string, finite number, or boolean`);
240
+ }
241
+ if (value === "." || value === "..") {
242
+ throw new Error(`API URL placeholder ${placeholder.path} cannot be a dot segment`);
243
+ }
244
+ if (placeholder.start === 0 && placeholder.path.startsWith("env.")) {
245
+ if (definitions[placeholder.path.slice(4)]?.type !== "url" || typeof value !== "string") {
246
+ throw new Error(`${placeholder.path}: only an env variable of type url can supply a base URL`);
247
+ }
248
+ validateEnvBaseUrl(value);
249
+ const suffix = url.slice(placeholder.end);
250
+ if (suffix && !suffix.startsWith("/")) throw new Error("Base URL placeholder must be followed by / or end of URL");
251
+ result += suffix.startsWith("/") ? value.replace(/\/+$/, "") : value;
252
+ } else {
253
+ result += url.slice(cursor, placeholder.start) + encodeURIComponent(String(value));
254
+ }
255
+ cursor = placeholder.end;
256
+ }
257
+ return result + url.slice(cursor);
258
+ }
259
+
48
260
  // src/compiler/compile-schema.ts
49
261
  var import_schema = require("@bsm-form/schema");
50
262
 
@@ -1347,7 +1559,25 @@ function collectDrawerSteps(steps, path, context, owningDrawerId) {
1347
1559
  });
1348
1560
  });
1349
1561
  }
1562
+ function validateApiUrlsInSteps(steps, path) {
1563
+ steps.forEach((step, index) => {
1564
+ const stepPath = `${path}[${index}]`;
1565
+ if (step.type === "when") {
1566
+ validateApiUrlsInSteps(step.then, `${stepPath}.then`);
1567
+ if (step.else) validateApiUrlsInSteps(step.else, `${stepPath}.else`);
1568
+ }
1569
+ if (step.type === "api") {
1570
+ try {
1571
+ validateApiUrlTemplate(step.request.url);
1572
+ } catch (error) {
1573
+ throw new Error(`Invalid API URL at ${stepPath}.request.url: ${error instanceof Error ? error.message : String(error)}`);
1574
+ }
1575
+ if (step.onError) validateApiUrlsInSteps(step.onError, `${stepPath}.onError`);
1576
+ }
1577
+ });
1578
+ }
1350
1579
  function collectDialogSteps(steps, path, context, owningDialogId) {
1580
+ validateApiUrlsInSteps(steps, path);
1351
1581
  steps.forEach((step, index) => {
1352
1582
  if (step.type !== "dialog") {
1353
1583
  return;
@@ -1445,6 +1675,7 @@ function validateDrawerStepReferences(context) {
1445
1675
  }
1446
1676
  function validateStepConditionReferences(context) {
1447
1677
  context.stepConditionReferences.forEach(({ path, field }) => {
1678
+ if (field.startsWith("env.")) return;
1448
1679
  if ((0, import_schema.isReservedConditionField)(field)) {
1449
1680
  if ((0, import_schema.isPendingActionConditionField)(field)) {
1450
1681
  const actionId = (0, import_schema.pendingActionIdFromField)(field);
@@ -1605,6 +1836,7 @@ var validationRegistry = {
1605
1836
  // src/engine/environment.ts
1606
1837
  function createFormEngineEnvironment(overrides = {}) {
1607
1838
  return {
1839
+ env: overrides.env ?? Object.freeze({}),
1608
1840
  request: overrides.request ?? defaultRequest,
1609
1841
  log: overrides.log ?? defaultLog,
1610
1842
  navigate: overrides.navigate ?? defaultNavigate,
@@ -1674,6 +1906,7 @@ function runInvokeValidator(rule, options) {
1674
1906
  const ctx = {
1675
1907
  values: options?.values ?? {},
1676
1908
  resources: options?.resources ?? {},
1909
+ env: options?.env ?? options?.environment?.env,
1677
1910
  ...options?.row !== void 0 ? { row: options.row } : {},
1678
1911
  ...options?.item !== void 0 ? { item: options.item } : {}
1679
1912
  };
@@ -1759,99 +1992,6 @@ function isResourcePathInFlight(sourceRelative, inFlight) {
1759
1992
  );
1760
1993
  }
1761
1994
 
1762
- // src/utils/get-path.ts
1763
- function pathSegments(path) {
1764
- const segments = [];
1765
- let i = 0;
1766
- while (i < path.length) {
1767
- const char = path[i];
1768
- if (char === ".") {
1769
- i += 1;
1770
- continue;
1771
- }
1772
- if (char === "[") {
1773
- const close = path.indexOf("]", i);
1774
- if (close === -1) {
1775
- segments.push(path.slice(i));
1776
- break;
1777
- }
1778
- segments.push(path.slice(i + 1, close));
1779
- i = close + 1;
1780
- continue;
1781
- }
1782
- let end = i;
1783
- while (end < path.length && path[end] !== "." && path[end] !== "[") {
1784
- end += 1;
1785
- }
1786
- if (end > i) {
1787
- segments.push(path.slice(i, end));
1788
- }
1789
- i = end;
1790
- }
1791
- return segments;
1792
- }
1793
- function getPath(obj, path) {
1794
- if (!path) {
1795
- return obj;
1796
- }
1797
- let current = obj;
1798
- for (const key of pathSegments(path)) {
1799
- if (typeof current !== "object" || current === null) {
1800
- return void 0;
1801
- }
1802
- current = current[key];
1803
- }
1804
- return current;
1805
- }
1806
-
1807
- // src/utils/scope-path.ts
1808
- function resolveScopedPath(ctx, path) {
1809
- if (path === "$values" || path === "values") {
1810
- return ctx.values;
1811
- }
1812
- if (path === "$resources" || path === "resources") {
1813
- return ctx.resources;
1814
- }
1815
- if (path === "$row" || path === "row") {
1816
- return ctx.row;
1817
- }
1818
- if (path === "$item" || path === "item") {
1819
- return ctx.item;
1820
- }
1821
- if (path.startsWith("values.")) {
1822
- return getPath(ctx.values, path.replace("values.", ""));
1823
- }
1824
- if (path.startsWith("resources.")) {
1825
- return getPath(ctx.resources, path.replace("resources.", ""));
1826
- }
1827
- if (path.startsWith("row.")) {
1828
- return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
1829
- }
1830
- if (path.startsWith("item.")) {
1831
- return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
1832
- }
1833
- return getPath(ctx.values, path);
1834
- }
1835
- function isExplicitScopedPath(path) {
1836
- return path === "$values" || path === "values" || path === "$resources" || path === "resources" || path === "$row" || path === "row" || path === "$item" || path === "item" || path.startsWith("values.") || path.startsWith("resources.") || path.startsWith("row.") || path.startsWith("item.");
1837
- }
1838
- function resolvePathStringsInData(value, ctx) {
1839
- if (typeof value === "string") {
1840
- return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
1841
- }
1842
- if (Array.isArray(value)) {
1843
- return value.map((item) => resolvePathStringsInData(item, ctx));
1844
- }
1845
- if (typeof value === "object" && value !== null) {
1846
- const result = {};
1847
- for (const [key, entry] of Object.entries(value)) {
1848
- result[key] = resolvePathStringsInData(entry, ctx);
1849
- }
1850
- return result;
1851
- }
1852
- return value;
1853
- }
1854
-
1855
1995
  // src/conditions/evaluate.ts
1856
1996
  function evaluateCondition(condition, values, state, options) {
1857
1997
  if ((0, import_schema2.isInvokeCondition)(condition)) {
@@ -1862,6 +2002,7 @@ function evaluateCondition(condition, values, state, options) {
1862
2002
  }
1863
2003
  const target = resolveConditionTarget(condition.field, values, state, {
1864
2004
  resources: options?.resources,
2005
+ env: options?.env ?? options?.environment?.env,
1865
2006
  ...options?.row !== void 0 ? { row: options.row } : {},
1866
2007
  ...options?.item !== void 0 ? { item: options.item } : {}
1867
2008
  });
@@ -1908,6 +2049,7 @@ function evaluateInvokeCondition(name, values, options) {
1908
2049
  const ctx = {
1909
2050
  values,
1910
2051
  resources: options?.resources ?? {},
2052
+ env: options?.env ?? options?.environment?.env,
1911
2053
  ...options?.row !== void 0 ? { row: options.row } : {},
1912
2054
  ...options?.item !== void 0 ? { item: options.item } : {}
1913
2055
  };
@@ -1962,6 +2104,7 @@ function resolveConditionTarget(field, values, state, options) {
1962
2104
  {
1963
2105
  values,
1964
2106
  resources: options?.resources ?? {},
2107
+ env: options?.env,
1965
2108
  ...options?.row !== void 0 ? { row: options.row } : {},
1966
2109
  ...options?.item !== void 0 ? { item: options.item } : {}
1967
2110
  },
@@ -2255,7 +2398,7 @@ var DependencyGraph = class {
2255
2398
  return ordered;
2256
2399
  }
2257
2400
  extractDependencies(conditions2) {
2258
- return conditions2.filter(import_schema4.isFieldCondition).map((condition) => condition.field).filter((field) => !(0, import_schema4.isReservedConditionField)(field));
2401
+ return conditions2.filter(import_schema4.isFieldCondition).map((condition) => condition.field).filter((field) => !(0, import_schema4.isReservedConditionField)(field) && !field.startsWith("env."));
2259
2402
  }
2260
2403
  clear() {
2261
2404
  this.forward.clear();
@@ -2277,10 +2420,52 @@ var EventBus = class {
2277
2420
  }
2278
2421
  };
2279
2422
 
2423
+ // src/utils/env-references.ts
2424
+ var pathKeys = /* @__PURE__ */ new Set(["from", "source", "field", "path", "bodyFrom", "queryFrom"]);
2425
+ function visit(value, path, key, data, transform) {
2426
+ if (typeof value === "string") {
2427
+ if (key === "url") return transform(value, path, true);
2428
+ if ((pathKeys.has(key) || path.endsWith(".bind.value") || data) && /^env\.[A-Za-z_]/.test(value)) return transform(value, path, false);
2429
+ return value;
2430
+ }
2431
+ if (Array.isArray(value)) return value.map((item, index) => visit(item, `${path}[${index}]`, key, data, transform));
2432
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [
2433
+ childKey,
2434
+ visit(child, `${path}.${childKey}`, childKey, data || childKey === "body" || childKey === "query", transform)
2435
+ ]));
2436
+ return value;
2437
+ }
2438
+ function collectFormEnvReferences(schema) {
2439
+ const references = [];
2440
+ visit(schema, "schema", "", false, (value, path, template) => {
2441
+ if (template) {
2442
+ for (const match of value.matchAll(/\$\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}/g)) {
2443
+ references.push({ name: match[1], path });
2444
+ }
2445
+ } else references.push({ name: value.slice(4).split(/[.[]/)[0], path });
2446
+ return value;
2447
+ });
2448
+ return references;
2449
+ }
2450
+ function validateFormEnvReferences(schema, definitions = {}) {
2451
+ for (const reference of collectFormEnvReferences(schema)) {
2452
+ if (!Object.hasOwn(definitions, reference.name)) throw new Error(`${reference.path}: undefined environment variable env.${reference.name}`);
2453
+ }
2454
+ }
2455
+ function remapFormEnvReferences(schema, previous, next) {
2456
+ return visit(schema, "schema", "", false, (value, _path, template) => {
2457
+ if (template) return value.replace(
2458
+ /\$\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}/g,
2459
+ (original, name) => name === previous ? "${env." + next + "}" : original
2460
+ );
2461
+ return value === `env.${previous}` ? `env.${next}` : value;
2462
+ });
2463
+ }
2464
+
2280
2465
  // src/pipeline/run-conditions.ts
2281
2466
  function runConditions(ctx) {
2282
- visit(ctx.tree, true);
2283
- function visit(node, parentVisible) {
2467
+ visit2(ctx.tree, true);
2468
+ function visit2(node, parentVisible) {
2284
2469
  const schema = node.schema;
2285
2470
  const hasVisibilityConditions = schema.kind === "field" || schema.kind === "display" || schema.kind === "action" || schema.kind === "layout" && schema.type === "step";
2286
2471
  if (!parentVisible) {
@@ -2298,7 +2483,7 @@ function runConditions(ctx) {
2298
2483
  }
2299
2484
  node.state.disabled = evaluateDisabled(schema, ctx);
2300
2485
  const childrenVisible = node.state.visible && !(schema.kind === "layout" && (schema.type === "dialog" || schema.type === "drawer") && node.state.open !== true);
2301
- node.children.forEach((child) => visit(child, childrenVisible));
2486
+ node.children.forEach((child) => visit2(child, childrenVisible));
2302
2487
  }
2303
2488
  }
2304
2489
  function evaluateDisabled(schema, ctx) {
@@ -2417,6 +2602,7 @@ function executeInvokeComputer(name, values, options) {
2417
2602
  const ctx = {
2418
2603
  values,
2419
2604
  resources: options?.resources ?? {},
2605
+ env: options?.env ?? options?.environment?.env,
2420
2606
  ...options?.row !== void 0 ? { row: options.row } : {},
2421
2607
  ...options?.item !== void 0 ? { item: options.item } : {}
2422
2608
  };
@@ -2765,6 +2951,7 @@ var StepRunner = class {
2765
2951
  (condition) => evaluateCondition(condition, values, conditionState, {
2766
2952
  environment: this.environment,
2767
2953
  resources: this.engine.getResources(),
2954
+ env: this.engine.getEnv(),
2768
2955
  ...options?.row !== void 0 ? { row: options.row } : {},
2769
2956
  ...options?.item !== void 0 ? { item: options.item } : {}
2770
2957
  })
@@ -2849,6 +3036,7 @@ var StepRunner = class {
2849
3036
  return {
2850
3037
  values: this.engine.getValues(),
2851
3038
  resources: this.engine.getResources(),
3039
+ env: this.engine.getEnv(),
2852
3040
  ...options?.row !== void 0 ? { row: options.row } : {},
2853
3041
  ...options?.item !== void 0 ? { item: options.item } : {}
2854
3042
  };
@@ -2857,6 +3045,7 @@ var StepRunner = class {
2857
3045
  return {
2858
3046
  values: this.engine.getValues(),
2859
3047
  resources: this.engine.getResources(),
3048
+ env: this.engine.getEnv(),
2860
3049
  ...options?.row !== void 0 ? { row: options.row } : {},
2861
3050
  ...options?.item !== void 0 ? { item: options.item } : {}
2862
3051
  };
@@ -2866,6 +3055,7 @@ function resolveApiRequestBody(step, engine, options) {
2866
3055
  const ctx = {
2867
3056
  values: engine.getValues(),
2868
3057
  resources: engine.getResources(),
3058
+ env: engine.getEnv(),
2869
3059
  ...options?.row !== void 0 ? { row: options.row } : {},
2870
3060
  ...options?.item !== void 0 ? { item: options.item } : {}
2871
3061
  };
@@ -2885,9 +3075,11 @@ function resolveApiRequestUrl(step, engine, options) {
2885
3075
  const ctx = {
2886
3076
  values: engine.getValues(),
2887
3077
  resources: engine.getResources(),
3078
+ env: engine.getEnv(),
2888
3079
  ...options?.row !== void 0 ? { row: options.row } : {},
2889
3080
  ...options?.item !== void 0 ? { item: options.item } : {}
2890
3081
  };
3082
+ const url = resolveApiUrlTemplate(step.request.url, ctx, engine.getEnvDefinitions());
2891
3083
  const hasQueryFrom = "queryFrom" in step.request && step.request.queryFrom !== void 0;
2892
3084
  const hasQuery = "query" in step.request && step.request.query !== void 0;
2893
3085
  if (hasQueryFrom && hasQuery) {
@@ -2903,7 +3095,7 @@ function resolveApiRequestUrl(step, engine, options) {
2903
3095
  } else if (hasQuery) {
2904
3096
  queryValue = resolvePathStringsInData(step.request.query, ctx);
2905
3097
  } else {
2906
- return step.request.url;
3098
+ return url;
2907
3099
  }
2908
3100
  if (queryValue === void 0 || queryValue === null || typeof queryValue !== "object" || Array.isArray(queryValue)) {
2909
3101
  throw new TypeError("api step query must resolve to an object");
@@ -2917,10 +3109,13 @@ function resolveApiRequestUrl(step, engine, options) {
2917
3109
  }
2918
3110
  const queryString = params.toString();
2919
3111
  if (!queryString) {
2920
- return step.request.url;
3112
+ return url;
2921
3113
  }
2922
- const separator = step.request.url.includes("?") ? "&" : "?";
2923
- return `${step.request.url}${separator}${queryString}`;
3114
+ const hashIndex = url.indexOf("#");
3115
+ const base = hashIndex < 0 ? url : url.slice(0, hashIndex);
3116
+ const hash = hashIndex < 0 ? "" : url.slice(hashIndex);
3117
+ const separator = base.includes("?") ? "&" : "?";
3118
+ return `${base}${separator}${queryString}${hash}`;
2924
3119
  }
2925
3120
  function toRequestBody(value, headers) {
2926
3121
  if (isBodyInit(value)) {
@@ -3177,11 +3372,11 @@ function collectTemplate(node, fields, nested) {
3177
3372
  }
3178
3373
  function collectValuesRepeatFieldIds(metas) {
3179
3374
  const ids = /* @__PURE__ */ new Set();
3180
- const visit = (meta) => {
3375
+ const visit2 = (meta) => {
3181
3376
  meta.fieldTemplates.forEach((node) => ids.add(node.id));
3182
- meta.nested.forEach(visit);
3377
+ meta.nested.forEach(visit2);
3183
3378
  };
3184
- metas.forEach(visit);
3379
+ metas.forEach(visit2);
3185
3380
  return ids;
3186
3381
  }
3187
3382
  function createDefaultRepeatItem(meta) {
@@ -3269,9 +3464,11 @@ function findValuesRepeatMeta(metas, repeatId) {
3269
3464
 
3270
3465
  // src/engine/form-engine.ts
3271
3466
  var FormEngine = class {
3272
- constructor(runtimeTree, environment = {}) {
3467
+ constructor(runtimeTree, environment = {}, envConfig = {}) {
3273
3468
  this.runtimeTree = runtimeTree;
3274
- this.environment = createFormEngineEnvironment(environment);
3469
+ this.envConfig = structuredClone(envConfig);
3470
+ validateFormEnvReferences(this.runtimeTree.schema, this.envConfig.definitions);
3471
+ this.environment = createFormEngineEnvironment({ ...environment, env: resolveFormEnv(this.envConfig, false) });
3275
3472
  this.stepRunner = new StepRunner(this, this.environment);
3276
3473
  this.buildNodeIndexes();
3277
3474
  this.buildDependencyGraph();
@@ -3297,6 +3494,13 @@ var FormEngine = class {
3297
3494
  graph = new DependencyGraph();
3298
3495
  stepRunner;
3299
3496
  environment;
3497
+ envConfig;
3498
+ getEnvDefinitions() {
3499
+ return structuredClone(this.envConfig.definitions ?? {});
3500
+ }
3501
+ getEnv() {
3502
+ return this.environment.env ?? Object.freeze({});
3503
+ }
3300
3504
  fieldNodes = /* @__PURE__ */ new Map();
3301
3505
  actionNodes = /* @__PURE__ */ new Map();
3302
3506
  displayNodes = /* @__PURE__ */ new Map();
@@ -3366,6 +3570,7 @@ var FormEngine = class {
3366
3570
  this.notifyStateUpdated();
3367
3571
  return this.batch(async () => {
3368
3572
  try {
3573
+ this.environment.env = resolveFormEnv(this.envConfig, !this.skipLoadStepsOnInitialize);
3369
3574
  this.hydrateValues(this.runtimeTree);
3370
3575
  if (!this.skipLoadStepsOnInitialize) {
3371
3576
  await this.runLoadSteps();
@@ -3813,7 +4018,8 @@ var FormEngine = class {
3813
4018
  getPath(
3814
4019
  {
3815
4020
  values: this.values,
3816
- resources: this.resources
4021
+ resources: this.resources,
4022
+ env: this.getEnv()
3817
4023
  },
3818
4024
  path
3819
4025
  )
@@ -3904,7 +4110,8 @@ var FormEngine = class {
3904
4110
  this.reevaluateConditions();
3905
4111
  const result = runSubmitPipeline(this.getSnapshot(), {
3906
4112
  environment: this.environment,
3907
- resources: this.resources
4113
+ resources: this.resources,
4114
+ env: this.getEnv()
3908
4115
  });
3909
4116
  const itemErrors = {};
3910
4117
  this.itemFieldState.forEach((state, name) => {
@@ -4295,7 +4502,8 @@ var FormEngine = class {
4295
4502
  value: changedField === "__bulk__" ? void 0 : nextValues[changedField],
4296
4503
  state: this.getConditionState(),
4297
4504
  environment: this.environment,
4298
- resources: this.resources
4505
+ resources: this.resources,
4506
+ env: this.getEnv()
4299
4507
  });
4300
4508
  this.validateValuesRepeatFields(
4301
4509
  changedFields.filter((name) => name.includes("."))
@@ -4321,7 +4529,8 @@ var FormEngine = class {
4321
4529
  value: void 0,
4322
4530
  state: this.getConditionState(),
4323
4531
  environment: this.environment,
4324
- resources: this.resources
4532
+ resources: this.resources,
4533
+ env: this.getEnv()
4325
4534
  };
4326
4535
  }
4327
4536
  getConditionState() {
@@ -4341,7 +4550,8 @@ var FormEngine = class {
4341
4550
  value: void 0,
4342
4551
  state: this.getConditionState(),
4343
4552
  environment: this.environment,
4344
- resources: this.resources
4553
+ resources: this.resources,
4554
+ env: this.getEnv()
4345
4555
  });
4346
4556
  }
4347
4557
  syncPendingState() {
@@ -4543,7 +4753,8 @@ var FormEngine = class {
4543
4753
  {
4544
4754
  environment: this.environment,
4545
4755
  values: this.values,
4546
- resources: this.resources
4756
+ resources: this.resources,
4757
+ env: this.getEnv()
4547
4758
  }
4548
4759
  );
4549
4760
  node.state.error = result.error;
@@ -4600,7 +4811,8 @@ var FormEngine = class {
4600
4811
  const source = resolveScopedPath(
4601
4812
  {
4602
4813
  values: this.values,
4603
- resources: this.resources
4814
+ resources: this.resources,
4815
+ env: this.getEnv()
4604
4816
  },
4605
4817
  options.source
4606
4818
  );
@@ -4627,6 +4839,7 @@ var FormEngine = class {
4627
4839
  {
4628
4840
  values: this.values,
4629
4841
  resources: this.resources,
4842
+ env: this.getEnv(),
4630
4843
  ...options?.item !== void 0 ? { item: options.item } : {}
4631
4844
  },
4632
4845
  data.source
@@ -4652,6 +4865,7 @@ var FormEngine = class {
4652
4865
  executeComputed(schema.computed, this.values, {
4653
4866
  environment: this.environment,
4654
4867
  resources: this.resources,
4868
+ env: this.getEnv(),
4655
4869
  ...options?.item !== void 0 ? { item: options.item } : {}
4656
4870
  })
4657
4871
  );
@@ -4663,6 +4877,7 @@ var FormEngine = class {
4663
4877
  {
4664
4878
  values: this.values,
4665
4879
  resources: this.resources,
4880
+ env: this.getEnv(),
4666
4881
  ...options?.item !== void 0 ? { item: options.item } : {}
4667
4882
  },
4668
4883
  props.from
@@ -4686,6 +4901,7 @@ var FormEngine = class {
4686
4901
  {
4687
4902
  values: this.values,
4688
4903
  resources: this.resources,
4904
+ env: this.getEnv(),
4689
4905
  ...options?.item !== void 0 ? { item: options.item } : {}
4690
4906
  },
4691
4907
  props.from
@@ -4717,6 +4933,7 @@ var FormEngine = class {
4717
4933
  {
4718
4934
  values: this.values,
4719
4935
  resources: this.resources,
4936
+ env: this.getEnv(),
4720
4937
  ...options?.item !== void 0 ? { item: options.item } : {}
4721
4938
  },
4722
4939
  props.titleFrom
@@ -4756,7 +4973,8 @@ var FormEngine = class {
4756
4973
  const source = resolveScopedPath(
4757
4974
  {
4758
4975
  values: this.values,
4759
- resources: this.resources
4976
+ resources: this.resources,
4977
+ env: this.getEnv()
4760
4978
  },
4761
4979
  props.source
4762
4980
  );
@@ -4945,6 +5163,7 @@ var FormEngine = class {
4945
5163
  {
4946
5164
  values: this.values,
4947
5165
  resources: this.resources,
5166
+ env: this.getEnv(),
4948
5167
  ...options.item !== void 0 ? { item: options.item } : {}
4949
5168
  },
4950
5169
  options.from.trim()
@@ -4963,7 +5182,7 @@ var FormEngine = class {
4963
5182
  }
4964
5183
  validateValuesRepeatFields(only) {
4965
5184
  const targets = only ? new Set(only) : void 0;
4966
- const visit = (meta, absolutePath) => {
5185
+ const visit2 = (meta, absolutePath) => {
4967
5186
  const items = readRepeatArray(this.values, absolutePath);
4968
5187
  items.forEach((item, index) => {
4969
5188
  meta.fieldTemplates.forEach((template, fieldName) => {
@@ -4981,20 +5200,21 @@ var FormEngine = class {
4981
5200
  const result = validateField(template.schema, value, {
4982
5201
  environment: this.environment,
4983
5202
  values: this.values,
4984
- resources: this.resources
5203
+ resources: this.resources,
5204
+ env: this.getEnv()
4985
5205
  });
4986
5206
  const state = this.ensureItemFieldState(scopedName);
4987
5207
  state.error = result.error;
4988
5208
  });
4989
5209
  meta.nested.forEach((nested) => {
4990
- visit(nested, `${absolutePath}.${index}.${nested.path}`);
5210
+ visit2(nested, `${absolutePath}.${index}.${nested.path}`);
4991
5211
  });
4992
5212
  });
4993
5213
  };
4994
- this.valuesRepeatMetas.forEach((meta) => visit(meta, meta.path));
5214
+ this.valuesRepeatMetas.forEach((meta) => visit2(meta, meta.path));
4995
5215
  }
4996
5216
  touchAllValuesRepeatFields() {
4997
- const visit = (meta, absolutePath) => {
5217
+ const visit2 = (meta, absolutePath) => {
4998
5218
  const items = readRepeatArray(this.values, absolutePath);
4999
5219
  items.forEach((_, index) => {
5000
5220
  meta.fieldTemplates.forEach((_2, fieldName) => {
@@ -5002,11 +5222,11 @@ var FormEngine = class {
5002
5222
  this.ensureItemFieldState(scopedName).touched = true;
5003
5223
  });
5004
5224
  meta.nested.forEach((nested) => {
5005
- visit(nested, `${absolutePath}.${index}.${nested.path}`);
5225
+ visit2(nested, `${absolutePath}.${index}.${nested.path}`);
5006
5226
  });
5007
5227
  });
5008
5228
  };
5009
- this.valuesRepeatMetas.forEach((meta) => visit(meta, meta.path));
5229
+ this.valuesRepeatMetas.forEach((meta) => visit2(meta, meta.path));
5010
5230
  }
5011
5231
  pruneItemFieldState(absolutePath, removedIndex, previousLength) {
5012
5232
  const next = /* @__PURE__ */ new Map();
@@ -5203,18 +5423,25 @@ function findNode(node, name) {
5203
5423
  adaptDisplay,
5204
5424
  adaptField,
5205
5425
  adaptLayout,
5426
+ collectFormEnvReferences,
5206
5427
  compileSchema,
5207
5428
  conditions,
5208
5429
  createFormEngineEnvironment,
5209
5430
  createRuntimeNode,
5210
5431
  formatTagSourceValue,
5211
5432
  isThenable,
5433
+ remapFormEnvReferences,
5434
+ resolveFormEnv,
5212
5435
  resolveLoadingProp,
5213
5436
  resolveTagValueMapping,
5214
5437
  runConditionEngine,
5215
5438
  tagMapKey,
5216
5439
  traverseNode,
5440
+ validateApiUrlTemplate,
5441
+ validateEnvBaseUrl,
5442
+ validateEnvDefinitions,
5217
5443
  validateField,
5444
+ validateFormEnvReferences,
5218
5445
  validateSchema,
5219
5446
  validation,
5220
5447
  validationRegistry