form0-core 0.1.26 → 0.2.1

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 CHANGED
@@ -1,82 +1,145 @@
1
1
  # form0-core
2
2
 
3
3
  [![NPM Version](https://img.shields.io/npm/v/form0-core)](https://www.npmjs.com/package/form0-core)
4
- [![NPM Downloads](https://img.shields.io/npm/dt/form0-core)](https://www.npmjs.com/package/form0-core)
4
+ [![NPM Downloads](https://img.shields.io/npm/dm/form0-core)](https://www.npmjs.com/package/form0-core)
5
+ [![CI](https://github.com/paqu-io/form0-core/actions/workflows/ci.yml/badge.svg)](https://github.com/paqu-io/form0-core/actions/workflows/ci.yml)
5
6
  ![NPM License](https://img.shields.io/npm/l/form0-core)
6
7
  [![Docs](https://img.shields.io/badge/docs-docs.form0.dev-2563eb)](https://docs.form0.dev)
7
8
  [![Website](https://img.shields.io/badge/site-form0.dev-0f172a)](https://form0.dev)
8
9
  ![NPM Last Update](https://img.shields.io/npm/last-update/form0-core)
10
+ [![Socket](https://socket.dev/api/badge/npm/package/form0-core)](https://socket.dev/npm/package/form0-core)
9
11
 
10
- > [!WARNING]
11
- > form0 is in active, very early development. Do not use in production. Expect breaking
12
- > changes and unstable behavior.
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 schema-driven engine that powers [form0 open-source ecosystem](https://form0.dev). It is framework-agnostic and runs in any JavaScript runtime (Node.js, browsers, React Native, etc.)
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 (recommended)
22
+ ## 🚀 Start with the CLI
17
23
 
18
- The entry point for most users is [form0-cli](https://github.com/paqu-io/form0-cli). Follow the [quickstart](https://docs.form0.dev/getting-started/quickstart) to create a project and preview
19
- your schema.
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
- ## 🗂️ Documentation
28
+ Use `form0-core` directly when you are building a renderer, integration, developer tool, or other
29
+ custom form runtime.
22
30
 
23
- - Overview: https://docs.form0.dev/core/overview
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
- `form0-core` owns behavioral schema concerns such as fields, conditions, calculations,
36
- events, and AI metadata. Applications may still attach optional top-level metadata such as
37
- `id` (unique form identifier), `status` (publication state), `version` (schema version),
38
- scope fields like `main_org_id`, and media or location settings. Operational counters like
39
- `record_count` and `record_last_change_at` should stay platform-owned and be injected at
40
- application or API boundaries, not treated as engine-authored schema. A top-level
41
- `form.version` may still exist, but the engine does not bump or consume it.
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
+ ## Schema and record ownership
85
+
86
+ `form0-core` owns behavioral schema concerns such as fields, conditions, calculations, events, and
87
+ AI metadata. Applications may attach optional top-level metadata such as form identifiers,
88
+ publication state, schema version, organization scope, and media or location settings.
89
+ Operational counters should remain platform-owned and be injected at application or API
90
+ boundaries.
42
91
 
43
92
  ### Record-side contract
44
93
 
45
- `form0-core` intentionally uses two different choice-value shapes:
94
+ `form0-core` intentionally uses two choice-value shapes:
46
95
 
47
- - Live engine / renderer values use renderer shape:
48
- - single / boolean: `{ choice, other }`
49
- - multi: `{ choices, other }`
50
- - Structured records use canonical stored shape:
51
- - single / boolean: `{ choice_value, other_value }`
52
- - multi: `{ choices_value, other_value }`
96
+ - Live engine and renderer values:
97
+ - single choice and boolean: `{ choice, other }`
98
+ - multiple choice: `{ choices, other }`
99
+ - Canonical structured records:
100
+ - single choice and boolean: `{ choice_value, other_value }`
101
+ - multiple choice: `{ choices_value, other_value }`
53
102
 
54
- Record-side utilities follow this contract:
103
+ Record utilities follow this contract:
55
104
 
56
- - `createStructuredRecord()` outputs canonical stored records
57
- - `normalizeStructuredRecord()` consumes and returns canonical stored records
58
- - `buildFormRecordSnapshot()` consumes canonical stored records and returns renderer snapshot values
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
105
+ - `createStructuredRecord()` produces canonical stored records.
106
+ - `normalizeStructuredRecord()` consumes and returns canonical stored records.
107
+ - `buildFormRecordSnapshot()` converts canonical records into renderer snapshot values.
108
+ - `projectDatasetRowValues()` consumes canonical stored rows.
63
109
 
64
110
  Record status remains top-level as `@status`; it is not stored inside `form_values`.
65
111
 
66
- ## Security
112
+ ## ✅ Requirements
113
+
114
+ - Node.js 22 or newer
115
+ - An ESM-capable runtime or bundler
116
+
117
+ ## 📚 Documentation
118
+
119
+ - [Core overview](https://docs.form0.dev/core/overview)
120
+ - [Core concepts](https://docs.form0.dev/core/concepts)
121
+ - [Full documentation](https://docs.form0.dev)
122
+
123
+ ## 🔒 Security
124
+
125
+ > [!CAUTION]
126
+ > Schema expressions and event scripts execute JavaScript. Only evaluate schemas from authors you
127
+ > trust. `SAFE` and `CUSTOM` modes provide validation controls; they are not security sandboxes.
67
128
 
68
- See `SECURITY.md` for security modes and configuration.
129
+ Read the [security policy](./SECURITY.md) before evaluating schemas outside a fully trusted
130
+ authoring workflow.
69
131
 
70
- ## Requirements
132
+ ## 🔗 Related repositories
71
133
 
72
- - Node.js 18+
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
- ## Related repositories
138
+ ## 🤝 Support and contributing
75
139
 
76
- - [form0-cli](https://github.com/paqu-io/form0-cli) - Command-line interface
77
- - [form0-react](https://github.com/paqu-io/form0-react) - React components
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
- ## Contributing
143
+ ## 📄 License
81
144
 
82
- Contributions are welcome! Please feel free to submit [issues](https://github.com/paqu-io/form0-core/issues) and [pull requests](https://github.com/paqu-io/form0-core/pulls).
145
+ [MIT](./LICENSE)
package/SECURITY.md CHANGED
@@ -1,189 +1,53 @@
1
- # Security Features
1
+ # Security Policy
2
2
 
3
- > [!CAUTION] > `SAFE` and `CUSTOM` mode are validation aids, not security sandboxes. The
4
- > evaluator compiles customer expressions with `new Function`; blocked-pattern
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
- form0-core provides configurable security options for expression evaluation to balance flexibility and safety.
6
+ ## Reporting a vulnerability
13
7
 
14
- ## Security Modes
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
- ### TRUSTED (Default)
12
+ Include:
17
13
 
18
- - **Full JavaScript access** - Current behavior maintained
19
- - **No restrictions** - All expressions execute as-is
20
- - **Use case**: Internal tools, trusted developers, client-side only
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
- ```javascript
23
- import { createFormEngine } from 'form0-core';
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
- // Default behavior - no security parameter needed
26
- const engine = createFormEngine({ schema });
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
- // Or explicitly set trusted mode
29
- const engine = createFormEngine({
30
- schema,
31
- security: { mode: 'trusted' },
32
- });
33
- ```
27
+ ## Trust model
34
28
 
35
- ### SAFE (validation only; not a trust boundary)
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
- - **Restricted context** - Only whitelisted globals available
38
- - **Pattern blocking** - Dangerous patterns are blocked
39
- - **Use case**: Diagnostics for trusted authors. It is not suitable for
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
- ```javascript
43
- import { createFormEngine, SECURITY_MODES, SAFE_SECURITY_CONFIG } from 'form0-core';
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
- // Simple safe mode
46
- const engine = createFormEngine({
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
- // Or use predefined safe config
52
- const engine = createFormEngine({
53
- schema,
54
- security: SAFE_SECURITY_CONFIG,
55
- });
56
- ```
45
+ ## Maintainer security documents
57
46
 
58
- ### CUSTOM
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
- - **User-defined rules** - Configure your own security settings
61
- - **Flexible restrictions** - Mix and match security features
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.1.26",
3
+ "version": "0.2.1",
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 \"**/*.{js,jsx,json,ts,tsx,md}\"",
13
- "format:check": "prettier --check \"**/*.{js,jsx,json,ts,tsx,md}\""
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": ">=18.0.0"
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
  }
@@ -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 = typeof baseKey === 'string' ? baseKey.replace(/^~+/, '').replace(/_+$/, '') : '';
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 (!metadata || typeof metadata.namingPolicy !== 'object' || Array.isArray(metadata.namingPolicy)) {
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 (!metadata || typeof metadata.choicePolicy !== 'object' || Array.isArray(metadata.choicePolicy)) {
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 (!metadata || typeof metadata.providerHints !== 'object' || Array.isArray(metadata.providerHints)) {
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
- return normalizedExpression.replace(/;+\s*$/u, '');
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 (el.type === 'Section' || el.type === 'RepeatableSection' || el.type === 'BuildingPlanSection') {
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('applyLinkedRecordSelection requires a values object from the form engine state');
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(`applyLinkedRecordSelection could not find field "${fieldIdentifier}" in the provided form`);
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(`applyLinkedRecordSelection can only target FormLinkField fields (received ${field.type})`);
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 = field.allow_multiple_records === true ? normalizedRecords : normalizedRecords.slice(0, 1);
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(selectedRecords, mapping.source_field_id, delimiter);
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)) return {
23
- repeatableSectionTree,
24
- fieldOwnership,
25
- sectionFields,
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') {
@@ -6,7 +6,8 @@
6
6
 
7
7
  function getRandomBytes(length) {
8
8
  const array = new Uint8Array(length);
9
- const cryptoObj = (typeof globalThis !== 'undefined' && (globalThis.crypto || globalThis.msCrypto)) || null;
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
- }