@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.js CHANGED
@@ -1,3 +1,208 @@
1
+ // src/utils/form-env.ts
2
+ function validateEnvBaseUrl(value) {
3
+ if (!value || /[\\\s?#]/.test(value) || value.startsWith("//")) throw new Error("Base URL cannot contain whitespace, query, fragment, or backslashes");
4
+ if (value.startsWith("/")) return;
5
+ let url;
6
+ try {
7
+ url = new URL(value);
8
+ } catch {
9
+ throw new Error("Expected an http(s) URL or path starting with /");
10
+ }
11
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) throw new Error("Base URL must use http(s) without credentials");
12
+ }
13
+ function validateValue(name, type, value) {
14
+ if (typeof value !== (type === "url" ? "string" : type) || typeof value === "number" && !Number.isFinite(value)) throw new Error(`env.${name}: expected ${String(type)}`);
15
+ if (type === "url") {
16
+ try {
17
+ validateEnvBaseUrl(value);
18
+ } catch (error) {
19
+ throw new Error(`env.${name}: ${error instanceof Error ? error.message : String(error)}`);
20
+ }
21
+ }
22
+ }
23
+ function validateEnvDefinitions(value) {
24
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Environment definitions must be an object");
25
+ for (const [name, entry] of Object.entries(value)) {
26
+ if (!/^[A-Za-z_]+$/.test(name) || ["__proto__", "prototype", "constructor"].includes(name)) throw new Error(`Invalid environment variable name: ${name}`);
27
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw new Error(`env.${name}: expected a definition`);
28
+ const definition = entry;
29
+ if (!["string", "number", "boolean", "url"].includes(String(definition.type))) throw new Error(`env.${name}: invalid type`);
30
+ for (const key of Object.keys(definition)) if (!["type", "label", "description", "default", "required", "allowOverride"].includes(key)) throw new Error(`env.${name}: unknown property ${key}`);
31
+ for (const key of ["required", "allowOverride"]) if (definition[key] !== void 0 && typeof definition[key] !== "boolean") throw new Error(`env.${name}.${key}: expected boolean`);
32
+ for (const key of ["label", "description"]) if (definition[key] !== void 0 && typeof definition[key] !== "string") throw new Error(`env.${name}.${key}: expected string`);
33
+ if (Object.hasOwn(definition, "default")) validateValue(name, definition.type, definition.default);
34
+ }
35
+ }
36
+ function resolveFormEnv(config = {}, requireValues = true) {
37
+ const definitions = config.definitions ?? {};
38
+ const overrides = config.overrides ?? {};
39
+ validateEnvDefinitions(definitions);
40
+ if (typeof overrides !== "object" || Array.isArray(overrides)) throw new Error("Environment overrides must be an object");
41
+ for (const name of Object.keys(overrides)) {
42
+ if (!Object.hasOwn(definitions, name)) throw new Error(`env.${name}: unknown override`);
43
+ if (definitions[name]?.allowOverride === false) throw new Error(`env.${name}: overrides are disabled`);
44
+ }
45
+ const values = {};
46
+ for (const [name, definition] of Object.entries(definitions)) {
47
+ const provided = Object.hasOwn(overrides, name);
48
+ const value = provided ? overrides[name] : definition.default;
49
+ if (provided || value !== void 0) validateValue(name, definition.type, value);
50
+ if (requireValues && definition.required && (value === void 0 || value === "")) throw new Error(`env.${name}: required value is missing`);
51
+ if (value !== void 0) values[name] = value;
52
+ }
53
+ return Object.freeze(values);
54
+ }
55
+
56
+ // src/utils/get-path.ts
57
+ function pathSegments(path) {
58
+ const segments = [];
59
+ let i = 0;
60
+ while (i < path.length) {
61
+ const char = path[i];
62
+ if (char === ".") {
63
+ i += 1;
64
+ continue;
65
+ }
66
+ if (char === "[") {
67
+ const close = path.indexOf("]", i);
68
+ if (close === -1) {
69
+ segments.push(path.slice(i));
70
+ break;
71
+ }
72
+ segments.push(path.slice(i + 1, close));
73
+ i = close + 1;
74
+ continue;
75
+ }
76
+ let end = i;
77
+ while (end < path.length && path[end] !== "." && path[end] !== "[") {
78
+ end += 1;
79
+ }
80
+ if (end > i) {
81
+ segments.push(path.slice(i, end));
82
+ }
83
+ i = end;
84
+ }
85
+ return segments;
86
+ }
87
+ function getPath(obj, path) {
88
+ if (!path) {
89
+ return obj;
90
+ }
91
+ let current = obj;
92
+ for (const key of pathSegments(path)) {
93
+ if (typeof current !== "object" || current === null) {
94
+ return void 0;
95
+ }
96
+ current = current[key];
97
+ }
98
+ return current;
99
+ }
100
+
101
+ // src/utils/scope-path.ts
102
+ function resolveScopedPath(ctx, path) {
103
+ if (path === "env") return ctx.env ?? {};
104
+ if (path.startsWith("env.")) return getPath(ctx.env ?? {}, path.slice(4));
105
+ if (path === "$values" || path === "values") {
106
+ return ctx.values;
107
+ }
108
+ if (path === "$resources" || path === "resources") {
109
+ return ctx.resources;
110
+ }
111
+ if (path === "$row" || path === "row") {
112
+ return ctx.row;
113
+ }
114
+ if (path === "$item" || path === "item") {
115
+ return ctx.item;
116
+ }
117
+ if (path.startsWith("values.")) {
118
+ return getPath(ctx.values, path.replace("values.", ""));
119
+ }
120
+ if (path.startsWith("resources.")) {
121
+ return getPath(ctx.resources, path.replace("resources.", ""));
122
+ }
123
+ if (path.startsWith("row.")) {
124
+ return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
125
+ }
126
+ if (path.startsWith("item.")) {
127
+ return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
128
+ }
129
+ return getPath(ctx.values, path);
130
+ }
131
+ function isExplicitScopedPath(path) {
132
+ 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.");
133
+ }
134
+ function resolvePathStringsInData(value, ctx) {
135
+ if (typeof value === "string") {
136
+ return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
137
+ }
138
+ if (Array.isArray(value)) {
139
+ return value.map((item) => resolvePathStringsInData(item, ctx));
140
+ }
141
+ if (typeof value === "object" && value !== null) {
142
+ const result = {};
143
+ for (const [key, entry] of Object.entries(value)) {
144
+ result[key] = resolvePathStringsInData(entry, ctx);
145
+ }
146
+ return result;
147
+ }
148
+ return value;
149
+ }
150
+
151
+ // src/utils/url-template.ts
152
+ var pathPattern = /^(values|resources|env)\.[A-Za-z_$][\w$]*(?:(?:\.(?:[A-Za-z_$][\w$]*|\d+))|(?:\[\d+\]))*$/;
153
+ function placeholders(url) {
154
+ const result = [];
155
+ let cursor = 0;
156
+ while (true) {
157
+ const start = url.indexOf("${", cursor);
158
+ if (start < 0) break;
159
+ const close = url.indexOf("}", start + 2);
160
+ if (close < 0) throw new Error("API URL template has an unclosed placeholder");
161
+ const path = url.slice(start + 2, close).trim();
162
+ if (!pathPattern.test(path)) {
163
+ throw new Error(`Invalid API URL placeholder: ${path}. Use a values.*, resources.*, or env.* data path`);
164
+ }
165
+ const origin = url.match(/^(?:[A-Za-z][A-Za-z0-9+.-]*:)?\/\/[^/?#]*/)?.[0];
166
+ if (start === 0 && !path.startsWith("env.") || origin && start < origin.length) {
167
+ throw new Error("API URL placeholders cannot replace the service origin; use a static base URL");
168
+ }
169
+ result.push({ start, end: close + 1, path });
170
+ cursor = close + 1;
171
+ }
172
+ return result;
173
+ }
174
+ function validateApiUrlTemplate(url) {
175
+ if (!url.trim()) throw new Error("API URL must not be empty");
176
+ placeholders(url);
177
+ }
178
+ function resolveApiUrlTemplate(url, context, definitions = {}) {
179
+ validateApiUrlTemplate(url);
180
+ let result = "";
181
+ let cursor = 0;
182
+ for (const placeholder of placeholders(url)) {
183
+ const value = resolveScopedPath(context, placeholder.path);
184
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" || typeof value === "number" && !Number.isFinite(value) || value === "") {
185
+ throw new Error(`API URL placeholder ${placeholder.path} must resolve to a non-empty string, finite number, or boolean`);
186
+ }
187
+ if (value === "." || value === "..") {
188
+ throw new Error(`API URL placeholder ${placeholder.path} cannot be a dot segment`);
189
+ }
190
+ if (placeholder.start === 0 && placeholder.path.startsWith("env.")) {
191
+ if (definitions[placeholder.path.slice(4)]?.type !== "url" || typeof value !== "string") {
192
+ throw new Error(`${placeholder.path}: only an env variable of type url can supply a base URL`);
193
+ }
194
+ validateEnvBaseUrl(value);
195
+ const suffix = url.slice(placeholder.end);
196
+ if (suffix && !suffix.startsWith("/")) throw new Error("Base URL placeholder must be followed by / or end of URL");
197
+ result += suffix.startsWith("/") ? value.replace(/\/+$/, "") : value;
198
+ } else {
199
+ result += url.slice(cursor, placeholder.start) + encodeURIComponent(String(value));
200
+ }
201
+ cursor = placeholder.end;
202
+ }
203
+ return result + url.slice(cursor);
204
+ }
205
+
1
206
  // src/compiler/compile-schema.ts
2
207
  import {
3
208
  fieldAllowsEvent,
@@ -1308,7 +1513,25 @@ function collectDrawerSteps(steps, path, context, owningDrawerId) {
1308
1513
  });
1309
1514
  });
1310
1515
  }
1516
+ function validateApiUrlsInSteps(steps, path) {
1517
+ steps.forEach((step, index) => {
1518
+ const stepPath = `${path}[${index}]`;
1519
+ if (step.type === "when") {
1520
+ validateApiUrlsInSteps(step.then, `${stepPath}.then`);
1521
+ if (step.else) validateApiUrlsInSteps(step.else, `${stepPath}.else`);
1522
+ }
1523
+ if (step.type === "api") {
1524
+ try {
1525
+ validateApiUrlTemplate(step.request.url);
1526
+ } catch (error) {
1527
+ throw new Error(`Invalid API URL at ${stepPath}.request.url: ${error instanceof Error ? error.message : String(error)}`);
1528
+ }
1529
+ if (step.onError) validateApiUrlsInSteps(step.onError, `${stepPath}.onError`);
1530
+ }
1531
+ });
1532
+ }
1311
1533
  function collectDialogSteps(steps, path, context, owningDialogId) {
1534
+ validateApiUrlsInSteps(steps, path);
1312
1535
  steps.forEach((step, index) => {
1313
1536
  if (step.type !== "dialog") {
1314
1537
  return;
@@ -1406,6 +1629,7 @@ function validateDrawerStepReferences(context) {
1406
1629
  }
1407
1630
  function validateStepConditionReferences(context) {
1408
1631
  context.stepConditionReferences.forEach(({ path, field }) => {
1632
+ if (field.startsWith("env.")) return;
1409
1633
  if (isReservedConditionField(field)) {
1410
1634
  if (isPendingActionConditionField(field)) {
1411
1635
  const actionId = pendingActionIdFromField(field);
@@ -1566,6 +1790,7 @@ var validationRegistry = {
1566
1790
  // src/engine/environment.ts
1567
1791
  function createFormEngineEnvironment(overrides = {}) {
1568
1792
  return {
1793
+ env: overrides.env ?? Object.freeze({}),
1569
1794
  request: overrides.request ?? defaultRequest,
1570
1795
  log: overrides.log ?? defaultLog,
1571
1796
  navigate: overrides.navigate ?? defaultNavigate,
@@ -1635,6 +1860,7 @@ function runInvokeValidator(rule, options) {
1635
1860
  const ctx = {
1636
1861
  values: options?.values ?? {},
1637
1862
  resources: options?.resources ?? {},
1863
+ env: options?.env ?? options?.environment?.env,
1638
1864
  ...options?.row !== void 0 ? { row: options.row } : {},
1639
1865
  ...options?.item !== void 0 ? { item: options.item } : {}
1640
1866
  };
@@ -1729,99 +1955,6 @@ function isResourcePathInFlight(sourceRelative, inFlight) {
1729
1955
  );
1730
1956
  }
1731
1957
 
1732
- // src/utils/get-path.ts
1733
- function pathSegments(path) {
1734
- const segments = [];
1735
- let i = 0;
1736
- while (i < path.length) {
1737
- const char = path[i];
1738
- if (char === ".") {
1739
- i += 1;
1740
- continue;
1741
- }
1742
- if (char === "[") {
1743
- const close = path.indexOf("]", i);
1744
- if (close === -1) {
1745
- segments.push(path.slice(i));
1746
- break;
1747
- }
1748
- segments.push(path.slice(i + 1, close));
1749
- i = close + 1;
1750
- continue;
1751
- }
1752
- let end = i;
1753
- while (end < path.length && path[end] !== "." && path[end] !== "[") {
1754
- end += 1;
1755
- }
1756
- if (end > i) {
1757
- segments.push(path.slice(i, end));
1758
- }
1759
- i = end;
1760
- }
1761
- return segments;
1762
- }
1763
- function getPath(obj, path) {
1764
- if (!path) {
1765
- return obj;
1766
- }
1767
- let current = obj;
1768
- for (const key of pathSegments(path)) {
1769
- if (typeof current !== "object" || current === null) {
1770
- return void 0;
1771
- }
1772
- current = current[key];
1773
- }
1774
- return current;
1775
- }
1776
-
1777
- // src/utils/scope-path.ts
1778
- function resolveScopedPath(ctx, path) {
1779
- if (path === "$values" || path === "values") {
1780
- return ctx.values;
1781
- }
1782
- if (path === "$resources" || path === "resources") {
1783
- return ctx.resources;
1784
- }
1785
- if (path === "$row" || path === "row") {
1786
- return ctx.row;
1787
- }
1788
- if (path === "$item" || path === "item") {
1789
- return ctx.item;
1790
- }
1791
- if (path.startsWith("values.")) {
1792
- return getPath(ctx.values, path.replace("values.", ""));
1793
- }
1794
- if (path.startsWith("resources.")) {
1795
- return getPath(ctx.resources, path.replace("resources.", ""));
1796
- }
1797
- if (path.startsWith("row.")) {
1798
- return ctx.row === void 0 ? void 0 : getPath(ctx.row, path.replace("row.", ""));
1799
- }
1800
- if (path.startsWith("item.")) {
1801
- return ctx.item === void 0 ? void 0 : getPath(ctx.item, path.replace("item.", ""));
1802
- }
1803
- return getPath(ctx.values, path);
1804
- }
1805
- function isExplicitScopedPath(path) {
1806
- 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.");
1807
- }
1808
- function resolvePathStringsInData(value, ctx) {
1809
- if (typeof value === "string") {
1810
- return isExplicitScopedPath(value) ? resolveScopedPath(ctx, value) : value;
1811
- }
1812
- if (Array.isArray(value)) {
1813
- return value.map((item) => resolvePathStringsInData(item, ctx));
1814
- }
1815
- if (typeof value === "object" && value !== null) {
1816
- const result = {};
1817
- for (const [key, entry] of Object.entries(value)) {
1818
- result[key] = resolvePathStringsInData(entry, ctx);
1819
- }
1820
- return result;
1821
- }
1822
- return value;
1823
- }
1824
-
1825
1958
  // src/conditions/evaluate.ts
1826
1959
  function evaluateCondition(condition, values, state, options) {
1827
1960
  if (isInvokeCondition(condition)) {
@@ -1832,6 +1965,7 @@ function evaluateCondition(condition, values, state, options) {
1832
1965
  }
1833
1966
  const target = resolveConditionTarget(condition.field, values, state, {
1834
1967
  resources: options?.resources,
1968
+ env: options?.env ?? options?.environment?.env,
1835
1969
  ...options?.row !== void 0 ? { row: options.row } : {},
1836
1970
  ...options?.item !== void 0 ? { item: options.item } : {}
1837
1971
  });
@@ -1878,6 +2012,7 @@ function evaluateInvokeCondition(name, values, options) {
1878
2012
  const ctx = {
1879
2013
  values,
1880
2014
  resources: options?.resources ?? {},
2015
+ env: options?.env ?? options?.environment?.env,
1881
2016
  ...options?.row !== void 0 ? { row: options.row } : {},
1882
2017
  ...options?.item !== void 0 ? { item: options.item } : {}
1883
2018
  };
@@ -1932,6 +2067,7 @@ function resolveConditionTarget(field, values, state, options) {
1932
2067
  {
1933
2068
  values,
1934
2069
  resources: options?.resources ?? {},
2070
+ env: options?.env,
1935
2071
  ...options?.row !== void 0 ? { row: options.row } : {},
1936
2072
  ...options?.item !== void 0 ? { item: options.item } : {}
1937
2073
  },
@@ -2231,7 +2367,7 @@ var DependencyGraph = class {
2231
2367
  return ordered;
2232
2368
  }
2233
2369
  extractDependencies(conditions2) {
2234
- return conditions2.filter(isFieldCondition2).map((condition) => condition.field).filter((field) => !isReservedConditionField3(field));
2370
+ return conditions2.filter(isFieldCondition2).map((condition) => condition.field).filter((field) => !isReservedConditionField3(field) && !field.startsWith("env."));
2235
2371
  }
2236
2372
  clear() {
2237
2373
  this.forward.clear();
@@ -2253,10 +2389,52 @@ var EventBus = class {
2253
2389
  }
2254
2390
  };
2255
2391
 
2392
+ // src/utils/env-references.ts
2393
+ var pathKeys = /* @__PURE__ */ new Set(["from", "source", "field", "path", "bodyFrom", "queryFrom"]);
2394
+ function visit(value, path, key, data, transform) {
2395
+ if (typeof value === "string") {
2396
+ if (key === "url") return transform(value, path, true);
2397
+ if ((pathKeys.has(key) || path.endsWith(".bind.value") || data) && /^env\.[A-Za-z_]/.test(value)) return transform(value, path, false);
2398
+ return value;
2399
+ }
2400
+ if (Array.isArray(value)) return value.map((item, index) => visit(item, `${path}[${index}]`, key, data, transform));
2401
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([childKey, child]) => [
2402
+ childKey,
2403
+ visit(child, `${path}.${childKey}`, childKey, data || childKey === "body" || childKey === "query", transform)
2404
+ ]));
2405
+ return value;
2406
+ }
2407
+ function collectFormEnvReferences(schema) {
2408
+ const references = [];
2409
+ visit(schema, "schema", "", false, (value, path, template) => {
2410
+ if (template) {
2411
+ for (const match of value.matchAll(/\$\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}/g)) {
2412
+ references.push({ name: match[1], path });
2413
+ }
2414
+ } else references.push({ name: value.slice(4).split(/[.[]/)[0], path });
2415
+ return value;
2416
+ });
2417
+ return references;
2418
+ }
2419
+ function validateFormEnvReferences(schema, definitions = {}) {
2420
+ for (const reference of collectFormEnvReferences(schema)) {
2421
+ if (!Object.hasOwn(definitions, reference.name)) throw new Error(`${reference.path}: undefined environment variable env.${reference.name}`);
2422
+ }
2423
+ }
2424
+ function remapFormEnvReferences(schema, previous, next) {
2425
+ return visit(schema, "schema", "", false, (value, _path, template) => {
2426
+ if (template) return value.replace(
2427
+ /\$\{\s*env\.([A-Za-z_][A-Za-z0-9_]*)\s*\}/g,
2428
+ (original, name) => name === previous ? "${env." + next + "}" : original
2429
+ );
2430
+ return value === `env.${previous}` ? `env.${next}` : value;
2431
+ });
2432
+ }
2433
+
2256
2434
  // src/pipeline/run-conditions.ts
2257
2435
  function runConditions(ctx) {
2258
- visit(ctx.tree, true);
2259
- function visit(node, parentVisible) {
2436
+ visit2(ctx.tree, true);
2437
+ function visit2(node, parentVisible) {
2260
2438
  const schema = node.schema;
2261
2439
  const hasVisibilityConditions = schema.kind === "field" || schema.kind === "display" || schema.kind === "action" || schema.kind === "layout" && schema.type === "step";
2262
2440
  if (!parentVisible) {
@@ -2274,7 +2452,7 @@ function runConditions(ctx) {
2274
2452
  }
2275
2453
  node.state.disabled = evaluateDisabled(schema, ctx);
2276
2454
  const childrenVisible = node.state.visible && !(schema.kind === "layout" && (schema.type === "dialog" || schema.type === "drawer") && node.state.open !== true);
2277
- node.children.forEach((child) => visit(child, childrenVisible));
2455
+ node.children.forEach((child) => visit2(child, childrenVisible));
2278
2456
  }
2279
2457
  }
2280
2458
  function evaluateDisabled(schema, ctx) {
@@ -2393,6 +2571,7 @@ function executeInvokeComputer(name, values, options) {
2393
2571
  const ctx = {
2394
2572
  values,
2395
2573
  resources: options?.resources ?? {},
2574
+ env: options?.env ?? options?.environment?.env,
2396
2575
  ...options?.row !== void 0 ? { row: options.row } : {},
2397
2576
  ...options?.item !== void 0 ? { item: options.item } : {}
2398
2577
  };
@@ -2741,6 +2920,7 @@ var StepRunner = class {
2741
2920
  (condition) => evaluateCondition(condition, values, conditionState, {
2742
2921
  environment: this.environment,
2743
2922
  resources: this.engine.getResources(),
2923
+ env: this.engine.getEnv(),
2744
2924
  ...options?.row !== void 0 ? { row: options.row } : {},
2745
2925
  ...options?.item !== void 0 ? { item: options.item } : {}
2746
2926
  })
@@ -2825,6 +3005,7 @@ var StepRunner = class {
2825
3005
  return {
2826
3006
  values: this.engine.getValues(),
2827
3007
  resources: this.engine.getResources(),
3008
+ env: this.engine.getEnv(),
2828
3009
  ...options?.row !== void 0 ? { row: options.row } : {},
2829
3010
  ...options?.item !== void 0 ? { item: options.item } : {}
2830
3011
  };
@@ -2833,6 +3014,7 @@ var StepRunner = class {
2833
3014
  return {
2834
3015
  values: this.engine.getValues(),
2835
3016
  resources: this.engine.getResources(),
3017
+ env: this.engine.getEnv(),
2836
3018
  ...options?.row !== void 0 ? { row: options.row } : {},
2837
3019
  ...options?.item !== void 0 ? { item: options.item } : {}
2838
3020
  };
@@ -2842,6 +3024,7 @@ function resolveApiRequestBody(step, engine, options) {
2842
3024
  const ctx = {
2843
3025
  values: engine.getValues(),
2844
3026
  resources: engine.getResources(),
3027
+ env: engine.getEnv(),
2845
3028
  ...options?.row !== void 0 ? { row: options.row } : {},
2846
3029
  ...options?.item !== void 0 ? { item: options.item } : {}
2847
3030
  };
@@ -2861,9 +3044,11 @@ function resolveApiRequestUrl(step, engine, options) {
2861
3044
  const ctx = {
2862
3045
  values: engine.getValues(),
2863
3046
  resources: engine.getResources(),
3047
+ env: engine.getEnv(),
2864
3048
  ...options?.row !== void 0 ? { row: options.row } : {},
2865
3049
  ...options?.item !== void 0 ? { item: options.item } : {}
2866
3050
  };
3051
+ const url = resolveApiUrlTemplate(step.request.url, ctx, engine.getEnvDefinitions());
2867
3052
  const hasQueryFrom = "queryFrom" in step.request && step.request.queryFrom !== void 0;
2868
3053
  const hasQuery = "query" in step.request && step.request.query !== void 0;
2869
3054
  if (hasQueryFrom && hasQuery) {
@@ -2879,7 +3064,7 @@ function resolveApiRequestUrl(step, engine, options) {
2879
3064
  } else if (hasQuery) {
2880
3065
  queryValue = resolvePathStringsInData(step.request.query, ctx);
2881
3066
  } else {
2882
- return step.request.url;
3067
+ return url;
2883
3068
  }
2884
3069
  if (queryValue === void 0 || queryValue === null || typeof queryValue !== "object" || Array.isArray(queryValue)) {
2885
3070
  throw new TypeError("api step query must resolve to an object");
@@ -2893,10 +3078,13 @@ function resolveApiRequestUrl(step, engine, options) {
2893
3078
  }
2894
3079
  const queryString = params.toString();
2895
3080
  if (!queryString) {
2896
- return step.request.url;
3081
+ return url;
2897
3082
  }
2898
- const separator = step.request.url.includes("?") ? "&" : "?";
2899
- return `${step.request.url}${separator}${queryString}`;
3083
+ const hashIndex = url.indexOf("#");
3084
+ const base = hashIndex < 0 ? url : url.slice(0, hashIndex);
3085
+ const hash = hashIndex < 0 ? "" : url.slice(hashIndex);
3086
+ const separator = base.includes("?") ? "&" : "?";
3087
+ return `${base}${separator}${queryString}${hash}`;
2900
3088
  }
2901
3089
  function toRequestBody(value, headers) {
2902
3090
  if (isBodyInit(value)) {
@@ -3153,11 +3341,11 @@ function collectTemplate(node, fields, nested) {
3153
3341
  }
3154
3342
  function collectValuesRepeatFieldIds(metas) {
3155
3343
  const ids = /* @__PURE__ */ new Set();
3156
- const visit = (meta) => {
3344
+ const visit2 = (meta) => {
3157
3345
  meta.fieldTemplates.forEach((node) => ids.add(node.id));
3158
- meta.nested.forEach(visit);
3346
+ meta.nested.forEach(visit2);
3159
3347
  };
3160
- metas.forEach(visit);
3348
+ metas.forEach(visit2);
3161
3349
  return ids;
3162
3350
  }
3163
3351
  function createDefaultRepeatItem(meta) {
@@ -3245,9 +3433,11 @@ function findValuesRepeatMeta(metas, repeatId) {
3245
3433
 
3246
3434
  // src/engine/form-engine.ts
3247
3435
  var FormEngine = class {
3248
- constructor(runtimeTree, environment = {}) {
3436
+ constructor(runtimeTree, environment = {}, envConfig = {}) {
3249
3437
  this.runtimeTree = runtimeTree;
3250
- this.environment = createFormEngineEnvironment(environment);
3438
+ this.envConfig = structuredClone(envConfig);
3439
+ validateFormEnvReferences(this.runtimeTree.schema, this.envConfig.definitions);
3440
+ this.environment = createFormEngineEnvironment({ ...environment, env: resolveFormEnv(this.envConfig, false) });
3251
3441
  this.stepRunner = new StepRunner(this, this.environment);
3252
3442
  this.buildNodeIndexes();
3253
3443
  this.buildDependencyGraph();
@@ -3273,6 +3463,13 @@ var FormEngine = class {
3273
3463
  graph = new DependencyGraph();
3274
3464
  stepRunner;
3275
3465
  environment;
3466
+ envConfig;
3467
+ getEnvDefinitions() {
3468
+ return structuredClone(this.envConfig.definitions ?? {});
3469
+ }
3470
+ getEnv() {
3471
+ return this.environment.env ?? Object.freeze({});
3472
+ }
3276
3473
  fieldNodes = /* @__PURE__ */ new Map();
3277
3474
  actionNodes = /* @__PURE__ */ new Map();
3278
3475
  displayNodes = /* @__PURE__ */ new Map();
@@ -3342,6 +3539,7 @@ var FormEngine = class {
3342
3539
  this.notifyStateUpdated();
3343
3540
  return this.batch(async () => {
3344
3541
  try {
3542
+ this.environment.env = resolveFormEnv(this.envConfig, !this.skipLoadStepsOnInitialize);
3345
3543
  this.hydrateValues(this.runtimeTree);
3346
3544
  if (!this.skipLoadStepsOnInitialize) {
3347
3545
  await this.runLoadSteps();
@@ -3789,7 +3987,8 @@ var FormEngine = class {
3789
3987
  getPath(
3790
3988
  {
3791
3989
  values: this.values,
3792
- resources: this.resources
3990
+ resources: this.resources,
3991
+ env: this.getEnv()
3793
3992
  },
3794
3993
  path
3795
3994
  )
@@ -3880,7 +4079,8 @@ var FormEngine = class {
3880
4079
  this.reevaluateConditions();
3881
4080
  const result = runSubmitPipeline(this.getSnapshot(), {
3882
4081
  environment: this.environment,
3883
- resources: this.resources
4082
+ resources: this.resources,
4083
+ env: this.getEnv()
3884
4084
  });
3885
4085
  const itemErrors = {};
3886
4086
  this.itemFieldState.forEach((state, name) => {
@@ -4271,7 +4471,8 @@ var FormEngine = class {
4271
4471
  value: changedField === "__bulk__" ? void 0 : nextValues[changedField],
4272
4472
  state: this.getConditionState(),
4273
4473
  environment: this.environment,
4274
- resources: this.resources
4474
+ resources: this.resources,
4475
+ env: this.getEnv()
4275
4476
  });
4276
4477
  this.validateValuesRepeatFields(
4277
4478
  changedFields.filter((name) => name.includes("."))
@@ -4297,7 +4498,8 @@ var FormEngine = class {
4297
4498
  value: void 0,
4298
4499
  state: this.getConditionState(),
4299
4500
  environment: this.environment,
4300
- resources: this.resources
4501
+ resources: this.resources,
4502
+ env: this.getEnv()
4301
4503
  };
4302
4504
  }
4303
4505
  getConditionState() {
@@ -4317,7 +4519,8 @@ var FormEngine = class {
4317
4519
  value: void 0,
4318
4520
  state: this.getConditionState(),
4319
4521
  environment: this.environment,
4320
- resources: this.resources
4522
+ resources: this.resources,
4523
+ env: this.getEnv()
4321
4524
  });
4322
4525
  }
4323
4526
  syncPendingState() {
@@ -4519,7 +4722,8 @@ var FormEngine = class {
4519
4722
  {
4520
4723
  environment: this.environment,
4521
4724
  values: this.values,
4522
- resources: this.resources
4725
+ resources: this.resources,
4726
+ env: this.getEnv()
4523
4727
  }
4524
4728
  );
4525
4729
  node.state.error = result.error;
@@ -4576,7 +4780,8 @@ var FormEngine = class {
4576
4780
  const source = resolveScopedPath(
4577
4781
  {
4578
4782
  values: this.values,
4579
- resources: this.resources
4783
+ resources: this.resources,
4784
+ env: this.getEnv()
4580
4785
  },
4581
4786
  options.source
4582
4787
  );
@@ -4603,6 +4808,7 @@ var FormEngine = class {
4603
4808
  {
4604
4809
  values: this.values,
4605
4810
  resources: this.resources,
4811
+ env: this.getEnv(),
4606
4812
  ...options?.item !== void 0 ? { item: options.item } : {}
4607
4813
  },
4608
4814
  data.source
@@ -4628,6 +4834,7 @@ var FormEngine = class {
4628
4834
  executeComputed(schema.computed, this.values, {
4629
4835
  environment: this.environment,
4630
4836
  resources: this.resources,
4837
+ env: this.getEnv(),
4631
4838
  ...options?.item !== void 0 ? { item: options.item } : {}
4632
4839
  })
4633
4840
  );
@@ -4639,6 +4846,7 @@ var FormEngine = class {
4639
4846
  {
4640
4847
  values: this.values,
4641
4848
  resources: this.resources,
4849
+ env: this.getEnv(),
4642
4850
  ...options?.item !== void 0 ? { item: options.item } : {}
4643
4851
  },
4644
4852
  props.from
@@ -4662,6 +4870,7 @@ var FormEngine = class {
4662
4870
  {
4663
4871
  values: this.values,
4664
4872
  resources: this.resources,
4873
+ env: this.getEnv(),
4665
4874
  ...options?.item !== void 0 ? { item: options.item } : {}
4666
4875
  },
4667
4876
  props.from
@@ -4693,6 +4902,7 @@ var FormEngine = class {
4693
4902
  {
4694
4903
  values: this.values,
4695
4904
  resources: this.resources,
4905
+ env: this.getEnv(),
4696
4906
  ...options?.item !== void 0 ? { item: options.item } : {}
4697
4907
  },
4698
4908
  props.titleFrom
@@ -4732,7 +4942,8 @@ var FormEngine = class {
4732
4942
  const source = resolveScopedPath(
4733
4943
  {
4734
4944
  values: this.values,
4735
- resources: this.resources
4945
+ resources: this.resources,
4946
+ env: this.getEnv()
4736
4947
  },
4737
4948
  props.source
4738
4949
  );
@@ -4921,6 +5132,7 @@ var FormEngine = class {
4921
5132
  {
4922
5133
  values: this.values,
4923
5134
  resources: this.resources,
5135
+ env: this.getEnv(),
4924
5136
  ...options.item !== void 0 ? { item: options.item } : {}
4925
5137
  },
4926
5138
  options.from.trim()
@@ -4939,7 +5151,7 @@ var FormEngine = class {
4939
5151
  }
4940
5152
  validateValuesRepeatFields(only) {
4941
5153
  const targets = only ? new Set(only) : void 0;
4942
- const visit = (meta, absolutePath) => {
5154
+ const visit2 = (meta, absolutePath) => {
4943
5155
  const items = readRepeatArray(this.values, absolutePath);
4944
5156
  items.forEach((item, index) => {
4945
5157
  meta.fieldTemplates.forEach((template, fieldName) => {
@@ -4957,20 +5169,21 @@ var FormEngine = class {
4957
5169
  const result = validateField(template.schema, value, {
4958
5170
  environment: this.environment,
4959
5171
  values: this.values,
4960
- resources: this.resources
5172
+ resources: this.resources,
5173
+ env: this.getEnv()
4961
5174
  });
4962
5175
  const state = this.ensureItemFieldState(scopedName);
4963
5176
  state.error = result.error;
4964
5177
  });
4965
5178
  meta.nested.forEach((nested) => {
4966
- visit(nested, `${absolutePath}.${index}.${nested.path}`);
5179
+ visit2(nested, `${absolutePath}.${index}.${nested.path}`);
4967
5180
  });
4968
5181
  });
4969
5182
  };
4970
- this.valuesRepeatMetas.forEach((meta) => visit(meta, meta.path));
5183
+ this.valuesRepeatMetas.forEach((meta) => visit2(meta, meta.path));
4971
5184
  }
4972
5185
  touchAllValuesRepeatFields() {
4973
- const visit = (meta, absolutePath) => {
5186
+ const visit2 = (meta, absolutePath) => {
4974
5187
  const items = readRepeatArray(this.values, absolutePath);
4975
5188
  items.forEach((_, index) => {
4976
5189
  meta.fieldTemplates.forEach((_2, fieldName) => {
@@ -4978,11 +5191,11 @@ var FormEngine = class {
4978
5191
  this.ensureItemFieldState(scopedName).touched = true;
4979
5192
  });
4980
5193
  meta.nested.forEach((nested) => {
4981
- visit(nested, `${absolutePath}.${index}.${nested.path}`);
5194
+ visit2(nested, `${absolutePath}.${index}.${nested.path}`);
4982
5195
  });
4983
5196
  });
4984
5197
  };
4985
- this.valuesRepeatMetas.forEach((meta) => visit(meta, meta.path));
5198
+ this.valuesRepeatMetas.forEach((meta) => visit2(meta, meta.path));
4986
5199
  }
4987
5200
  pruneItemFieldState(absolutePath, removedIndex, previousLength) {
4988
5201
  const next = /* @__PURE__ */ new Map();
@@ -5178,18 +5391,25 @@ export {
5178
5391
  adaptDisplay,
5179
5392
  adaptField,
5180
5393
  adaptLayout,
5394
+ collectFormEnvReferences,
5181
5395
  compileSchema,
5182
5396
  conditions,
5183
5397
  createFormEngineEnvironment,
5184
5398
  createRuntimeNode,
5185
5399
  formatTagSourceValue,
5186
5400
  isThenable,
5401
+ remapFormEnvReferences,
5402
+ resolveFormEnv,
5187
5403
  resolveLoadingProp,
5188
5404
  resolveTagValueMapping,
5189
5405
  runConditionEngine,
5190
5406
  tagMapKey,
5191
5407
  traverseNode,
5408
+ validateApiUrlTemplate,
5409
+ validateEnvBaseUrl,
5410
+ validateEnvDefinitions,
5192
5411
  validateField,
5412
+ validateFormEnvReferences,
5193
5413
  validateSchema,
5194
5414
  validation,
5195
5415
  validationRegistry