form0-core 0.1.26 → 0.2.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/README.md +111 -48
- package/SECURITY.md +38 -174
- package/package.json +17 -5
- package/src/builtins/registry.js +1 -4
- package/src/schema/ai-metadata-validator.js +1 -3
- package/src/schema/building-plan-expander.js +2 -1
- package/src/utilities/ai-metadata.js +15 -3
- package/src/utilities/calculation-dependencies.js +1 -6
- package/src/utilities/calculation-expression-utils.js +11 -1
- package/src/utilities/field-helpers.js +5 -1
- package/src/utilities/form-event-editor.js +2 -10
- package/src/utilities/form-link-helpers.js +16 -5
- package/src/utilities/repeatable-helpers.js +6 -5
- package/src/utilities/uuid.js +2 -1
- package/scripts/legacy-postinstall-message.cjs +0 -42
package/README.md
CHANGED
|
@@ -1,82 +1,145 @@
|
|
|
1
1
|
# form0-core
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/form0-core)
|
|
4
|
-
[](https://www.npmjs.com/package/form0-core)
|
|
5
|
+
[](https://github.com/paqu-io/form0-core/actions/workflows/ci.yml)
|
|
5
6
|

|
|
6
7
|
[](https://docs.form0.dev)
|
|
7
8
|
[](https://form0.dev)
|
|
8
9
|

|
|
10
|
+
[](https://socket.dev/npm/package/form0-core)
|
|
9
11
|
|
|
10
|
-
> [!
|
|
11
|
-
> form0 is in active
|
|
12
|
-
>
|
|
12
|
+
> [!NOTE]
|
|
13
|
+
> form0 is in active development and is available to use today. Its schema format and core
|
|
14
|
+
> concepts are stable in practice, but releases before 1.0 may include breaking changes. Pin your
|
|
15
|
+
> versions and review the release notes when upgrading. A formally stable release is coming.
|
|
13
16
|
|
|
14
|
-
form0-core is the
|
|
17
|
+
`form0-core` is the framework-agnostic, schema-driven engine behind the
|
|
18
|
+
[form0 open-source ecosystem](https://form0.dev). It evaluates calculations, conditional
|
|
19
|
+
visibility, requirements, read-only rules, validation, and form events in Node.js, browsers, and
|
|
20
|
+
React Native.
|
|
15
21
|
|
|
16
|
-
## 🚀 Start with the CLI
|
|
22
|
+
## 🚀 Start with the CLI
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
Most users should begin with [`form0-cli`](https://github.com/paqu-io/form0-cli) rather than install
|
|
25
|
+
the engine directly. Follow the [quickstart](https://docs.form0.dev/getting-started/quickstart) to
|
|
26
|
+
create a project, edit a schema, and preview a form.
|
|
20
27
|
|
|
21
|
-
|
|
28
|
+
Use `form0-core` directly when you are building a renderer, integration, developer tool, or other
|
|
29
|
+
custom form runtime.
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
- Concepts: https://docs.form0.dev/core/concepts
|
|
25
|
-
- Full docs: https://docs.form0.dev
|
|
26
|
-
|
|
27
|
-
## Direct usage (advanced)
|
|
28
|
-
|
|
29
|
-
If you are integrating the engine directly:
|
|
31
|
+
## 📦 Installation
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
34
|
npm install form0-core
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
## ⚡ Quick example
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import { createFormEngine } from 'form0-core';
|
|
41
|
+
|
|
42
|
+
const schema = {
|
|
43
|
+
form: {
|
|
44
|
+
name: 'Contact form',
|
|
45
|
+
status_field: null,
|
|
46
|
+
elements: [
|
|
47
|
+
{
|
|
48
|
+
type: 'TextField',
|
|
49
|
+
key: 'name',
|
|
50
|
+
data_name: 'name',
|
|
51
|
+
label: 'Name',
|
|
52
|
+
display: 'default',
|
|
53
|
+
description: null,
|
|
54
|
+
description_mode: null,
|
|
55
|
+
required: true,
|
|
56
|
+
required_conditions: null,
|
|
57
|
+
visible: true,
|
|
58
|
+
visible_conditions: null,
|
|
59
|
+
read_only: false,
|
|
60
|
+
read_only_conditions: null,
|
|
61
|
+
default_value: null,
|
|
62
|
+
pattern: null,
|
|
63
|
+
pattern_description: null,
|
|
64
|
+
supporting_image: false,
|
|
65
|
+
supporting_image_path: null,
|
|
66
|
+
supporting_image_display: null,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const engine = createFormEngine({
|
|
73
|
+
schema,
|
|
74
|
+
initialValues: { name: 'Ada' },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
engine.eval();
|
|
78
|
+
console.log(engine.getState());
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The engine API exposes `eval()` for calculations, conditions, and validation; `trigger()` for form
|
|
82
|
+
events; and `getState()` for current values and evaluated field state.
|
|
83
|
+
|
|
84
|
+
## 🔒 Security
|
|
85
|
+
|
|
86
|
+
> [!CAUTION]
|
|
87
|
+
> Schema expressions and event scripts execute JavaScript. Only evaluate schemas from authors you
|
|
88
|
+
> trust. `SAFE` and `CUSTOM` modes provide validation controls; they are not security sandboxes.
|
|
89
|
+
|
|
90
|
+
Read the [security policy](./SECURITY.md) before evaluating schemas outside a fully trusted
|
|
91
|
+
authoring workflow.
|
|
92
|
+
|
|
93
|
+
## Schema and record ownership
|
|
94
|
+
|
|
95
|
+
`form0-core` owns behavioral schema concerns such as fields, conditions, calculations, events, and
|
|
96
|
+
AI metadata. Applications may attach optional top-level metadata such as form identifiers,
|
|
97
|
+
publication state, schema version, organization scope, and media or location settings.
|
|
98
|
+
Operational counters should remain platform-owned and be injected at application or API
|
|
99
|
+
boundaries.
|
|
42
100
|
|
|
43
101
|
### Record-side contract
|
|
44
102
|
|
|
45
|
-
`form0-core` intentionally uses two
|
|
103
|
+
`form0-core` intentionally uses two choice-value shapes:
|
|
46
104
|
|
|
47
|
-
- Live engine
|
|
48
|
-
- single
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
- single
|
|
52
|
-
-
|
|
105
|
+
- Live engine and renderer values:
|
|
106
|
+
- single choice and boolean: `{ choice, other }`
|
|
107
|
+
- multiple choice: `{ choices, other }`
|
|
108
|
+
- Canonical structured records:
|
|
109
|
+
- single choice and boolean: `{ choice_value, other_value }`
|
|
110
|
+
- multiple choice: `{ choices_value, other_value }`
|
|
53
111
|
|
|
54
|
-
Record
|
|
112
|
+
Record utilities follow this contract:
|
|
55
113
|
|
|
56
|
-
- `createStructuredRecord()`
|
|
57
|
-
- `normalizeStructuredRecord()` consumes and returns canonical stored records
|
|
58
|
-
- `buildFormRecordSnapshot()`
|
|
59
|
-
- `projectDatasetRowValues()` consumes canonical stored rows
|
|
60
|
-
- Choice `FIELD_SPECS` keep separate validators by context:
|
|
61
|
-
- `valueValidator` validates live engine / renderer values
|
|
62
|
-
- `recordValueValidator` validates canonical stored record values
|
|
114
|
+
- `createStructuredRecord()` produces canonical stored records.
|
|
115
|
+
- `normalizeStructuredRecord()` consumes and returns canonical stored records.
|
|
116
|
+
- `buildFormRecordSnapshot()` converts canonical records into renderer snapshot values.
|
|
117
|
+
- `projectDatasetRowValues()` consumes canonical stored rows.
|
|
63
118
|
|
|
64
119
|
Record status remains top-level as `@status`; it is not stored inside `form_values`.
|
|
65
120
|
|
|
66
|
-
##
|
|
121
|
+
## ✅ Requirements
|
|
122
|
+
|
|
123
|
+
- Node.js 22 or newer
|
|
124
|
+
- An ESM-capable runtime or bundler
|
|
125
|
+
|
|
126
|
+
## 📚 Documentation
|
|
67
127
|
|
|
68
|
-
|
|
128
|
+
- [Core overview](https://docs.form0.dev/core/overview)
|
|
129
|
+
- [Core concepts](https://docs.form0.dev/core/concepts)
|
|
130
|
+
- [Full documentation](https://docs.form0.dev)
|
|
69
131
|
|
|
70
|
-
##
|
|
132
|
+
## 🔗 Related repositories
|
|
71
133
|
|
|
72
|
-
-
|
|
134
|
+
- [form0-cli](https://github.com/paqu-io/form0-cli) — recommended project entry point
|
|
135
|
+
- [form0-react](https://github.com/paqu-io/form0-react) — React bindings and renderers
|
|
136
|
+
- [form0-react-native](https://github.com/paqu-io/form0-react-native) — React Native bindings and renderers
|
|
73
137
|
|
|
74
|
-
##
|
|
138
|
+
## 🤝 Support and contributing
|
|
75
139
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
- [form0-react-native](https://github.com/paqu-io/form0-react-native) - React Native components
|
|
140
|
+
See [SUPPORT.md](https://github.com/paqu-io/form0-core/blob/main/SUPPORT.md) for help and
|
|
141
|
+
[CONTRIBUTING.md](https://github.com/paqu-io/form0-core/blob/main/CONTRIBUTING.md) to contribute.
|
|
79
142
|
|
|
80
|
-
##
|
|
143
|
+
## 📄 License
|
|
81
144
|
|
|
82
|
-
|
|
145
|
+
[MIT](./LICENSE)
|
package/SECURITY.md
CHANGED
|
@@ -1,189 +1,53 @@
|
|
|
1
|
-
# Security
|
|
1
|
+
# Security Policy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
> checks can be bypassed through dynamic property access, and
|
|
6
|
-
> `maxExecutionTime` is not currently enforced. Do not execute untrusted
|
|
7
|
-
> schemas in a privileged server or browser context. See
|
|
8
|
-
> `analyses/SAFE_MODE_SECURITY_ANALYSIS.md` for the accepted critical finding
|
|
9
|
-
> and `analyses/UNTRUSTED_JAVASCRIPT_EXECUTION_ARCHITECTURE.md` for the selected
|
|
10
|
-
> isolation architecture that preserves vanilla JavaScript and imperative event scripts.
|
|
3
|
+
Security reports are taken seriously. Please report vulnerabilities privately so they can be
|
|
4
|
+
investigated and fixed before public disclosure.
|
|
11
5
|
|
|
12
|
-
|
|
6
|
+
## Reporting a vulnerability
|
|
13
7
|
|
|
14
|
-
|
|
8
|
+
Use the
|
|
9
|
+
[form0-core private vulnerability report](https://github.com/paqu-io/form0-core/security/advisories/new).
|
|
10
|
+
Do not open a public issue for a suspected vulnerability.
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
Include:
|
|
17
13
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
14
|
+
- the affected version and runtime;
|
|
15
|
+
- a minimal reproduction or proof of concept;
|
|
16
|
+
- the impact you believe is possible;
|
|
17
|
+
- any mitigations you have already identified; and
|
|
18
|
+
- whether the issue has been disclosed anywhere else.
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
Reports affecting any version are welcome. When possible, reproduce the issue with the latest
|
|
21
|
+
release. Security fixes are normally released for the latest version; older versions are assessed
|
|
22
|
+
case by case.
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Maintainers will review the report, may ask for more information, and will coordinate disclosure
|
|
25
|
+
after a fix or mitigation is available. Please keep the report private during that process.
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
const engine = createFormEngine({
|
|
30
|
-
schema,
|
|
31
|
-
security: { mode: 'trusted' },
|
|
32
|
-
});
|
|
33
|
-
```
|
|
27
|
+
## Trust model
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
form0-core supports JavaScript expressions and imperative event scripts. These expressions and
|
|
30
|
+
scripts are executable code, so schemas that contain them must come from authors you trust.
|
|
36
31
|
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
|
|
40
|
-
adversarial or otherwise untrusted expressions.
|
|
32
|
+
**SAFE** and **CUSTOM** modes provide validation and configuration controls. They are not security
|
|
33
|
+
sandboxes, and pattern-based restrictions must not be used as a boundary for executing adversarial
|
|
34
|
+
schemas in a privileged browser, server, CLI, or mobile context.
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
Giving an untrusted user control over executable schema expressions and then observing that the
|
|
37
|
+
expressions can execute JavaScript is outside this project's security boundary. Reports remain in
|
|
38
|
+
scope when behavior crosses a documented boundary—for example, when data that is not intended to
|
|
39
|
+
be executable becomes code, or when an attacker can affect a schema or runtime context they were
|
|
40
|
+
not authorized to control.
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
schema,
|
|
48
|
-
security: { mode: SECURITY_MODES.SAFE },
|
|
49
|
-
});
|
|
42
|
+
Applications are responsible for authenticating schema authors, authorizing schema changes, and
|
|
43
|
+
isolating execution when schemas cannot be trusted.
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
const engine = createFormEngine({
|
|
53
|
-
schema,
|
|
54
|
-
security: SAFE_SECURITY_CONFIG,
|
|
55
|
-
});
|
|
56
|
-
```
|
|
45
|
+
## Maintainer security documents
|
|
57
46
|
|
|
58
|
-
|
|
47
|
+
The public documents in
|
|
48
|
+
[analyses](https://github.com/paqu-io/form0-core/tree/main/analyses) record the current threat model,
|
|
49
|
+
known limitations, and the planned isolation architecture. They are maintainer-oriented
|
|
50
|
+
engineering documents, not claims that the current validation modes provide isolation.
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
-
|
|
62
|
-
- **Use case**: Specific security requirements
|
|
63
|
-
|
|
64
|
-
```javascript
|
|
65
|
-
const engine = createFormEngine({
|
|
66
|
-
schema,
|
|
67
|
-
security: {
|
|
68
|
-
mode: SECURITY_MODES.CUSTOM,
|
|
69
|
-
maxExecutionTime: 1000,
|
|
70
|
-
allowedGlobals: ['Math', 'Date', 'Number'],
|
|
71
|
-
blockedPatterns: [/\bwindow\b/, /\bdocument\b/, /\bfetch\b/],
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Security Configuration Options
|
|
77
|
-
|
|
78
|
-
```javascript
|
|
79
|
-
// TRUSTED mode (default) - no additional config needed
|
|
80
|
-
const trustedConfig = {
|
|
81
|
-
mode: 'trusted', // Full JavaScript access
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// SAFE mode - uses predefined safe settings
|
|
85
|
-
const safeConfig = {
|
|
86
|
-
mode: 'safe', // Automatically applies safe defaults
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
// CUSTOM mode - define your own rules
|
|
90
|
-
const customConfig = {
|
|
91
|
-
mode: 'custom',
|
|
92
|
-
maxExecutionTime: 1000, // Milliseconds (future feature)
|
|
93
|
-
maxCallStackDepth: 100, // Maximum recursion depth (future feature)
|
|
94
|
-
allowedGlobals: ['Math', 'Date', 'JSON'], // Whitelisted global objects
|
|
95
|
-
blockedPatterns: [/\beval\b/, /\bwindow\b/], // Regex patterns to block
|
|
96
|
-
};
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Default Blocked Patterns (Safe Mode)
|
|
100
|
-
|
|
101
|
-
The following patterns are blocked by default in safe mode:
|
|
102
|
-
|
|
103
|
-
- `eval`, `Function` - Code execution
|
|
104
|
-
- `window`, `document` - Browser globals
|
|
105
|
-
- `process`, `require` - Node.js globals
|
|
106
|
-
- `fetch`, `XMLHttpRequest` - Network requests
|
|
107
|
-
- `localStorage`, `sessionStorage` - Storage APIs
|
|
108
|
-
- `__proto__`, `constructor`, `prototype` - Prototype pollution
|
|
109
|
-
|
|
110
|
-
## Migration Guide
|
|
111
|
-
|
|
112
|
-
### No Changes Needed
|
|
113
|
-
|
|
114
|
-
Existing code continues to work unchanged:
|
|
115
|
-
|
|
116
|
-
```javascript
|
|
117
|
-
// This still works exactly as before
|
|
118
|
-
const engine = createFormEngine({ schema, initialValues });
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Adding Security
|
|
122
|
-
|
|
123
|
-
To add security, simply include the security parameter:
|
|
124
|
-
|
|
125
|
-
```javascript
|
|
126
|
-
// Add validation-oriented safe mode. This is not a sandbox.
|
|
127
|
-
const engine = createFormEngine({
|
|
128
|
-
schema,
|
|
129
|
-
initialValues,
|
|
130
|
-
security: { mode: 'safe' },
|
|
131
|
-
});
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Examples
|
|
135
|
-
|
|
136
|
-
### Safe Mathematical Calculations
|
|
137
|
-
|
|
138
|
-
```javascript
|
|
139
|
-
const schema = {
|
|
140
|
-
form: {
|
|
141
|
-
elements: [
|
|
142
|
-
{
|
|
143
|
-
type: 'CalculatedField',
|
|
144
|
-
data_name: 'result',
|
|
145
|
-
calculate: 'Math.max($value1, $value2) * 1.1', // ✅ Works in safe mode
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
### Blocked Dangerous Expressions
|
|
153
|
-
|
|
154
|
-
```javascript
|
|
155
|
-
const schema = {
|
|
156
|
-
form: {
|
|
157
|
-
elements: [
|
|
158
|
-
{
|
|
159
|
-
type: 'CalculatedField',
|
|
160
|
-
data_name: 'result',
|
|
161
|
-
calculate: 'window.alert("hello")', // ❌ Blocked in safe mode
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
};
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Custom Security Rules
|
|
169
|
-
|
|
170
|
-
```javascript
|
|
171
|
-
const engine = createFormEngine({
|
|
172
|
-
schema,
|
|
173
|
-
security: {
|
|
174
|
-
mode: 'custom',
|
|
175
|
-
allowedGlobals: ['Math'], // Only Math allowed
|
|
176
|
-
blockedPatterns: [/\bDate\b/], // Block Date usage
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
## Testing Security
|
|
182
|
-
|
|
183
|
-
Use the included test file to verify security behavior:
|
|
184
|
-
|
|
185
|
-
```bash
|
|
186
|
-
node test/security-test.js
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
This will show how different expressions behave under different security modes.
|
|
52
|
+
For ordinary usage questions, see
|
|
53
|
+
[SUPPORT.md](https://github.com/paqu-io/form0-core/blob/main/SUPPORT.md).
|
package/package.json
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
7
|
"description": "The schema-driven form engine behind form0, the open-source form ecosystem by paqu.io. Framework-agnostic: conditional visibility, validation and calculations from a JSON schema, in Node.js, browsers and React Native.",
|
|
8
8
|
"main": "src/index.js",
|
|
9
9
|
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"SECURITY.md"
|
|
13
|
+
],
|
|
10
14
|
"scripts": {
|
|
11
|
-
"test": "node tests/form0-core.test.js && node tests/numeric-field-schema.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/title-fields.test.js && node tests/builtin-metadata.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/security-test.js",
|
|
12
|
-
"format": "prettier --write \"
|
|
13
|
-
"format:check": "prettier --check \"
|
|
15
|
+
"test": "node tests/form0-core.test.js && node tests/numeric-field-schema.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/title-fields.test.js && node tests/builtin-metadata.test.js && node tests/calculation-expression-utils.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/security-test.js",
|
|
16
|
+
"format": "prettier --write \"src/**/*.{js,jsx}\" \"tests/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" package.json README.md SECURITY.md SUPPORT.md CONTRIBUTING.md eslint.config.js .github/workflows/ci.yml .github/workflows/publish.yml",
|
|
17
|
+
"format:check": "prettier --check \"src/**/*.{js,jsx}\" \"tests/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" package.json README.md SECURITY.md SUPPORT.md CONTRIBUTING.md eslint.config.js .github/workflows/ci.yml .github/workflows/publish.yml",
|
|
18
|
+
"lint:check": "eslint src tests",
|
|
19
|
+
"package:check": "node scripts/check-package-contents.mjs",
|
|
20
|
+
"security:audit:prod": "npm audit --omit=dev --omit=peer --audit-level=high",
|
|
21
|
+
"check": "npm run format:check && npm run lint:check && npm test",
|
|
22
|
+
"release:check": "npm run check && npm run security:audit:prod && npm run package:check"
|
|
14
23
|
},
|
|
15
24
|
"exports": {
|
|
16
25
|
".": {
|
|
@@ -46,9 +55,12 @@
|
|
|
46
55
|
},
|
|
47
56
|
"homepage": "https://form0.dev",
|
|
48
57
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
58
|
+
"node": ">=22.0.0"
|
|
50
59
|
},
|
|
51
60
|
"devDependencies": {
|
|
61
|
+
"@eslint/js": "^10.0.1",
|
|
62
|
+
"eslint": "^10.5.0",
|
|
63
|
+
"globals": "^17.12.0",
|
|
52
64
|
"ignore": "^7.0.5",
|
|
53
65
|
"prettier": "^3.5.3"
|
|
54
66
|
}
|
package/src/builtins/registry.js
CHANGED
|
@@ -118,10 +118,7 @@ export const calculationBuiltins = buildBuiltinObject([
|
|
|
118
118
|
...CALCULATION_ONLY_ENTRIES,
|
|
119
119
|
]);
|
|
120
120
|
|
|
121
|
-
export const eventBuiltins = buildBuiltinObject([
|
|
122
|
-
...COMMON_RUNTIME_ENTRIES,
|
|
123
|
-
...EVENT_ONLY_ENTRIES,
|
|
124
|
-
]);
|
|
121
|
+
export const eventBuiltins = buildBuiltinObject([...COMMON_RUNTIME_ENTRIES, ...EVENT_ONLY_ENTRIES]);
|
|
125
122
|
|
|
126
123
|
// Backward-compatible alias for calculation runtime helpers.
|
|
127
124
|
export const builtins = calculationBuiltins;
|
|
@@ -211,9 +211,7 @@ function validateFillStrategy(strategy, errors, prefix) {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
if (!FILL_STRATEGIES.has(strategy)) {
|
|
214
|
-
errors.push(
|
|
215
|
-
`${prefix}fillStrategy must be one of: ${Array.from(FILL_STRATEGIES).join(', ')}`
|
|
216
|
-
);
|
|
214
|
+
errors.push(`${prefix}fillStrategy must be one of: ${Array.from(FILL_STRATEGIES).join(', ')}`);
|
|
217
215
|
}
|
|
218
216
|
}
|
|
219
217
|
|
|
@@ -19,7 +19,8 @@ function appendSuffix(base, suffix, maxLength = null) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function createScopedKey(baseKey, suffix) {
|
|
22
|
-
const cleanBase =
|
|
22
|
+
const cleanBase =
|
|
23
|
+
typeof baseKey === 'string' ? baseKey.replace(/^~+/, '').replace(/_+$/, '') : '';
|
|
23
24
|
const scopedBase = appendSuffix(cleanBase, suffix);
|
|
24
25
|
return `~${scopedBase}`;
|
|
25
26
|
}
|
|
@@ -38,7 +38,11 @@ export function getFormAITasks(form) {
|
|
|
38
38
|
|
|
39
39
|
export function getFormAINamingPolicy(form) {
|
|
40
40
|
const metadata = getFormAIMetadata(form);
|
|
41
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
!metadata ||
|
|
43
|
+
typeof metadata.namingPolicy !== 'object' ||
|
|
44
|
+
Array.isArray(metadata.namingPolicy)
|
|
45
|
+
) {
|
|
42
46
|
return null;
|
|
43
47
|
}
|
|
44
48
|
return metadata.namingPolicy;
|
|
@@ -78,7 +82,11 @@ export function isFieldAIInferrable(field) {
|
|
|
78
82
|
|
|
79
83
|
export function getFieldAIChoicePolicy(field) {
|
|
80
84
|
const metadata = getFieldAIMetadata(field);
|
|
81
|
-
if (
|
|
85
|
+
if (
|
|
86
|
+
!metadata ||
|
|
87
|
+
typeof metadata.choicePolicy !== 'object' ||
|
|
88
|
+
Array.isArray(metadata.choicePolicy)
|
|
89
|
+
) {
|
|
82
90
|
return null;
|
|
83
91
|
}
|
|
84
92
|
return metadata.choicePolicy;
|
|
@@ -86,7 +94,11 @@ export function getFieldAIChoicePolicy(field) {
|
|
|
86
94
|
|
|
87
95
|
export function getFieldAIProviderHints(field) {
|
|
88
96
|
const metadata = getFieldAIMetadata(field);
|
|
89
|
-
if (
|
|
97
|
+
if (
|
|
98
|
+
!metadata ||
|
|
99
|
+
typeof metadata.providerHints !== 'object' ||
|
|
100
|
+
Array.isArray(metadata.providerHints)
|
|
101
|
+
) {
|
|
90
102
|
return null;
|
|
91
103
|
}
|
|
92
104
|
return metadata.providerHints;
|
|
@@ -260,12 +260,7 @@ function extractEvalCallArguments(code) {
|
|
|
260
260
|
|
|
261
261
|
if (char === "'" && !inDoubleQuote && !inBacktick && !isEscapedCharacter(source, i)) {
|
|
262
262
|
inSingleQuote = !inSingleQuote;
|
|
263
|
-
} else if (
|
|
264
|
-
char === '"' &&
|
|
265
|
-
!inSingleQuote &&
|
|
266
|
-
!inBacktick &&
|
|
267
|
-
!isEscapedCharacter(source, i)
|
|
268
|
-
) {
|
|
263
|
+
} else if (char === '"' && !inSingleQuote && !inBacktick && !isEscapedCharacter(source, i)) {
|
|
269
264
|
inDoubleQuote = !inDoubleQuote;
|
|
270
265
|
} else if (
|
|
271
266
|
char === '`' &&
|
|
@@ -10,6 +10,16 @@ export function isMultilineCalculationExpression(expression) {
|
|
|
10
10
|
|
|
11
11
|
export function normalizeInlineCalculationExpression(expression) {
|
|
12
12
|
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
13
|
+
const expressionWithoutTrailingWhitespace = normalizedExpression.trimEnd();
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
if (!expressionWithoutTrailingWhitespace.endsWith(';')) {
|
|
16
|
+
return normalizedExpression;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let expressionEnd = expressionWithoutTrailingWhitespace.length;
|
|
20
|
+
while (expressionEnd > 0 && expressionWithoutTrailingWhitespace[expressionEnd - 1] === ';') {
|
|
21
|
+
expressionEnd -= 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return expressionWithoutTrailingWhitespace.slice(0, expressionEnd);
|
|
15
25
|
}
|
|
@@ -10,7 +10,11 @@ export function flattenFields(elements) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
return elements.flatMap((el) => {
|
|
13
|
-
if (
|
|
13
|
+
if (
|
|
14
|
+
el.type === 'Section' ||
|
|
15
|
+
el.type === 'RepeatableSection' ||
|
|
16
|
+
el.type === 'BuildingPlanSection'
|
|
17
|
+
) {
|
|
14
18
|
return [el, ...flattenFields(el.elements)];
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -17,11 +17,7 @@ const COMMON_GLOBAL_FUNCTIONS = new Set([
|
|
|
17
17
|
'Boolean',
|
|
18
18
|
]);
|
|
19
19
|
|
|
20
|
-
const STRUCTURAL_FIELD_TYPES = new Set([
|
|
21
|
-
'Section',
|
|
22
|
-
'RepeatableSection',
|
|
23
|
-
'BuildingPlanSection',
|
|
24
|
-
]);
|
|
20
|
+
const STRUCTURAL_FIELD_TYPES = new Set(['Section', 'RepeatableSection', 'BuildingPlanSection']);
|
|
25
21
|
|
|
26
22
|
const FORM_EVENT_BUILTIN_STATUS_BY_NAME = Object.freeze({
|
|
27
23
|
EVAL: 'advanced',
|
|
@@ -296,11 +292,7 @@ export function getFormEventReferenceCatalog({ schema }) {
|
|
|
296
292
|
* referencedFields: string[],
|
|
297
293
|
* }}
|
|
298
294
|
*/
|
|
299
|
-
export function analyzeFormEventCode({
|
|
300
|
-
code,
|
|
301
|
-
schema,
|
|
302
|
-
securityConfig = DEFAULT_SECURITY_CONFIG,
|
|
303
|
-
}) {
|
|
295
|
+
export function analyzeFormEventCode({ code, schema, securityConfig = DEFAULT_SECURITY_CONFIG }) {
|
|
304
296
|
const issues = [];
|
|
305
297
|
const issueKeys = new Set();
|
|
306
298
|
const usedBuiltins = [];
|
|
@@ -235,16 +235,22 @@ export function applyLinkedRecordSelection({
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
if (!values || typeof values !== 'object') {
|
|
238
|
-
throw new Error(
|
|
238
|
+
throw new Error(
|
|
239
|
+
'applyLinkedRecordSelection requires a values object from the form engine state'
|
|
240
|
+
);
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
const field = resolveFormField(form, fieldIdentifier);
|
|
242
244
|
if (!field) {
|
|
243
|
-
throw new Error(
|
|
245
|
+
throw new Error(
|
|
246
|
+
`applyLinkedRecordSelection could not find field "${fieldIdentifier}" in the provided form`
|
|
247
|
+
);
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
if (field.type !== 'FormLinkField') {
|
|
247
|
-
throw new Error(
|
|
251
|
+
throw new Error(
|
|
252
|
+
`applyLinkedRecordSelection can only target FormLinkField fields (received ${field.type})`
|
|
253
|
+
);
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
const normalizedRecords = normalizeLinkedRecords(records);
|
|
@@ -255,7 +261,8 @@ export function applyLinkedRecordSelection({
|
|
|
255
261
|
);
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
const selectedRecords =
|
|
264
|
+
const selectedRecords =
|
|
265
|
+
field.allow_multiple_records === true ? normalizedRecords : normalizedRecords.slice(0, 1);
|
|
259
266
|
|
|
260
267
|
values[field.data_name] = selectedRecords.map((record) => ({
|
|
261
268
|
record_id: record.record_id,
|
|
@@ -275,7 +282,11 @@ export function applyLinkedRecordSelection({
|
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
if (field.allow_multiple_records === true) {
|
|
278
|
-
const aggregated = aggregateDefaultValues(
|
|
285
|
+
const aggregated = aggregateDefaultValues(
|
|
286
|
+
selectedRecords,
|
|
287
|
+
mapping.source_field_id,
|
|
288
|
+
delimiter
|
|
289
|
+
);
|
|
279
290
|
values[destinationField.data_name] = aggregated;
|
|
280
291
|
return;
|
|
281
292
|
}
|
|
@@ -19,11 +19,12 @@ export function buildRepeatableSectionTree(
|
|
|
19
19
|
fieldOwnership = new Map(),
|
|
20
20
|
sectionFields = new Set()
|
|
21
21
|
) {
|
|
22
|
-
if (!Array.isArray(elements))
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
if (!Array.isArray(elements))
|
|
23
|
+
return {
|
|
24
|
+
repeatableSectionTree,
|
|
25
|
+
fieldOwnership,
|
|
26
|
+
sectionFields,
|
|
27
|
+
};
|
|
27
28
|
|
|
28
29
|
elements.forEach((element) => {
|
|
29
30
|
if (element.type === 'Section' || element.type === 'BuildingPlanSection') {
|
package/src/utilities/uuid.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
function getRandomBytes(length) {
|
|
8
8
|
const array = new Uint8Array(length);
|
|
9
|
-
const cryptoObj =
|
|
9
|
+
const cryptoObj =
|
|
10
|
+
(typeof globalThis !== 'undefined' && (globalThis.crypto || globalThis.msCrypto)) || null;
|
|
10
11
|
|
|
11
12
|
if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
|
|
12
13
|
cryptoObj.getRandomValues(array);
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Legacy reference only.
|
|
2
|
-
// This script is intentionally not wired into package.json lifecycle hooks.
|
|
3
|
-
// It is retained as a reference for the previous install-time guidance message.
|
|
4
|
-
|
|
5
|
-
const PACKAGE_NAME = 'form0-core';
|
|
6
|
-
|
|
7
|
-
const npmUserAgent = process.env.npm_config_user_agent || '';
|
|
8
|
-
const npmArgv = process.env.npm_config_argv;
|
|
9
|
-
|
|
10
|
-
const isSupportedManager = /(npm|pnpm|yarn|bun)\//.test(npmUserAgent);
|
|
11
|
-
|
|
12
|
-
const parseNpmArgv = (raw) => {
|
|
13
|
-
if (!raw) return null;
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(raw);
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const includesExplicitPackage = (argv) => {
|
|
22
|
-
if (!argv) return false;
|
|
23
|
-
const args = Array.isArray(argv.original) ? argv.original : argv.cooked || [];
|
|
24
|
-
return args.some(
|
|
25
|
-
(arg) => arg === PACKAGE_NAME || arg.startsWith(`${PACKAGE_NAME}@`),
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
if (isSupportedManager && includesExplicitPackage(parseNpmArgv(npmArgv))) {
|
|
30
|
-
// Keep message short to avoid noisy installs.
|
|
31
|
-
console.warn(
|
|
32
|
-
[
|
|
33
|
-
'',
|
|
34
|
-
'form0-core is the engine only.',
|
|
35
|
-
'For the full form0 ecosystem, install the CLI:',
|
|
36
|
-
' npm install -g form0-cli',
|
|
37
|
-
'Docs: https://docs.form0.dev/getting-started/quickstart',
|
|
38
|
-
'If you are integrating the engine directly, you can ignore this warning.',
|
|
39
|
-
'',
|
|
40
|
-
].join('\n'),
|
|
41
|
-
);
|
|
42
|
-
}
|