diff --git a/CLAUDE.md b/CLAUDE.md
index 5b3b397..5d75feb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,11 +18,13 @@ The repository ships a Claude skills library at [plugin/skills/](plugin/skills/)
The library's default "build me an app" entry point is [`skills/formio-application/`](skills/formio-application/) — a framework-agnostic orchestrator that runs the full pipeline from plain-language intent through `formio-resource-planner`, the `project_import` MCP tool, and handoff to a framework-specific scaffolding skill. Today the only framework implementor is [`skills/formio-angular/`](skills/formio-angular/) (with its sub-skill [`skills/formio-angular/resources/`](skills/formio-angular/resources/) for per-resource NgModule work); `formio-angular` is no longer a top-level build-an-app skill — it is the Angular-specific implementor that `formio-application` delegates to. Future framework skills (`formio-react`, etc.) add themselves as rows in `skills/formio-application/FRAMEWORK.md`'s registry table; no other change to the orchestrator is required.
+The library also ships [`skills/formio-sdk/`](plugin/skills/formio-sdk/) — a source-derived reference for the `@formio/js` SDK and `@formio/js/utils` Utilities, authored directly from the Form.io source code (`packages/core/src/sdk`, `packages/core/src/utils`, `packages/formio.js/src/Formio.js`, `packages/formio.js/src/utils`) rather than from drift-prone online docs. Reference docs cover SDK setup, auth, forms, submissions, projects, roles, files, plugins, VanillaJS rendering (`Formio.createForm`), and the Utils surface (Evaluator, form traversal, conditions, logic, JSONLogic, mask/sanitize, misc). The skill mandates ESM imports (`import { Formio } from '@formio/js'`, `import { Utils } from '@formio/js/utils'`) and explicit Hosted-vs-SaaS URL configuration.
+
The router skill's `description` follows a three-clause template: capability statement, a "Use when the user asks to …" trigger clause, and a "Not for: …" negative-trigger clause disambiguating from `formio-application` (orchestrator) and `formio-resource-planner` (planner). Each reference document includes a `## MCP Tool Preference` section instructing Claude to prefer the MCP server's first-party tools (`form_*`, `role_*`, `project_*`, `authenticate`) when they cover the requested operation.
Authentication: the MCP server uses a browser-based portal-login flow — a short-lived local Express server renders the Form.io portal login form and captures the returned JWT via a `/callback` endpoint; `formioFetch` then attaches `x-jwt-token` on every request. Skills do NOT use PKCE or API-key auth.
-Skills are validated by [packages/mcp-server/src/skills-validator.ts](packages/mcp-server/src/skills-validator.ts); `pnpm test` fails if the router's frontmatter drifts, the router description is missing its trigger or negative-trigger clause, any required reference file is missing or empty, any reference doc drifts from the required heading layout, the canonical portal-login JWT auth paragraph is missing (except in `server-status.md`), or scope-consistency rules are violated. Terminology is strict: `baseUrl`/`base_url` refers only to `FORMIO_BASE_URL`; `projectUrl`/`project_url` refers only to `FORMIO_PROJECT_URL`.
+Skill authoring conventions (not enforced by automated tests): the router's frontmatter and three-clause description, required reference files present and non-empty, the required reference-doc heading layout, the canonical portal-login JWT auth paragraph (except in `server-status.md`), and scope consistency. Terminology is strict: `baseUrl`/`base_url` refers only to `FORMIO_BASE_URL`; `projectUrl`/`project_url` refers only to `FORMIO_PROJECT_URL`.
## Iterating on skills
diff --git a/openspec/changes/archive/2026-04-16-add-user-auth/design.md b/openspec/changes/archive/2026-04-16-add-user-auth/design.md
index 19a1737..f16bff8 100644
--- a/openspec/changes/archive/2026-04-16-add-user-auth/design.md
+++ b/openspec/changes/archive/2026-04-16-add-user-auth/design.md
@@ -56,7 +56,7 @@ Structure: `{ "": "" }`
**Why `~/.formio/`**: Follows the convention of CLI tools storing config in a dotfile directory in the user's home. Keeps it separate from project-level config.
-### 4. Token validation via `GET {projectUrl}/current`
+### 4. Token validation via `GET {baseUrl}/current`
On startup (after reading config and cache), send a request to the `/current` endpoint. If it returns 200, the token is valid. If 401, clear the cached token and trigger the login flow (or fail in API key mode).
diff --git a/openspec/changes/archive/2026-04-16-add-user-auth/proposal.md b/openspec/changes/archive/2026-04-16-add-user-auth/proposal.md
index 57b02f1..26bdec7 100644
--- a/openspec/changes/archive/2026-04-16-add-user-auth/proposal.md
+++ b/openspec/changes/archive/2026-04-16-add-user-auth/proposal.md
@@ -6,7 +6,7 @@ The MCP server currently authenticates all API requests using a shared admin API
- Add an authentication module that spins up an ephemeral Express server, renders the project's login form via the Form.io SDK, captures the user's JWT on successful login, and shuts down
- Add token caching to disk (`~/.formio/mcp-tokens.json`) keyed by project URL so users don't re-authenticate on every MCP server restart
-- Add a startup token validation step that hits `GET {projectUrl}/current` to check if a cached or provided token is still valid
+- Add a startup token validation step that hits `GET {baseUrl}/current` to check if a cached or provided token is still valid
- **BREAKING**: `FORMIO_API_KEY` becomes optional instead of required — if not provided, the server triggers the browser login flow
- Add `FORMIO_LOGIN_FORM` optional env var to override the default login form URL (`{projectUrl}/user/login`)
- Modify `formioFetch` to send `x-jwt-token` header when using a user JWT, falling back to `x-token` when using an API key
diff --git a/openspec/changes/archive/2026-04-16-add-user-auth/specs/token-validation/spec.md b/openspec/changes/archive/2026-04-16-add-user-auth/specs/token-validation/spec.md
index d81f3cc..c7a4bfe 100644
--- a/openspec/changes/archive/2026-04-16-add-user-auth/specs/token-validation/spec.md
+++ b/openspec/changes/archive/2026-04-16-add-user-auth/specs/token-validation/spec.md
@@ -2,31 +2,31 @@
### Requirement: Token is validated on startup via GET /current
-On startup, the MCP server SHALL validate any available token (cached JWT or API key) by sending a request to `GET {projectUrl}/current` with the appropriate auth header. A 200 response means the token is valid.
+On startup, the MCP server SHALL validate any available token (cached JWT or API key) by sending a request to `GET {baseUrl}/current` with the appropriate auth header. A 200 response means the token is valid.
#### Scenario: Valid JWT token on startup
- **WHEN** the server starts with a cached JWT that is still valid
-- **THEN** `GET {projectUrl}/current` returns 200
+- **THEN** `GET {baseUrl}/current` returns 200
- **AND** the server proceeds without triggering the login flow
#### Scenario: Valid API key on startup
- **WHEN** the server starts with `FORMIO_API_KEY` set to a valid key
-- **THEN** `GET {projectUrl}/current` with `x-token` header returns 200
+- **THEN** `GET {baseUrl}/current` with `x-token` header returns 200
- **AND** the server proceeds normally
#### Scenario: Expired JWT triggers login flow
- **WHEN** the server starts with a cached JWT that has expired
-- **THEN** `GET {projectUrl}/current` returns 401
+- **THEN** `GET {baseUrl}/current` returns 401
- **AND** the cached token is cleared
- **AND** the login flow is triggered
#### Scenario: Invalid API key fails with error
- **WHEN** the server starts with `FORMIO_API_KEY` set to an invalid key
-- **THEN** `GET {projectUrl}/current` returns 401
+- **THEN** `GET {baseUrl}/current` returns 401
- **AND** the server throws an error indicating the API key is invalid
#### Scenario: No token and no API key triggers login flow
diff --git a/openspec/changes/archive/2026-04-16-add-user-auth/tasks.md b/openspec/changes/archive/2026-04-16-add-user-auth/tasks.md
index 48c7116..b8516d4 100644
--- a/openspec/changes/archive/2026-04-16-add-user-auth/tasks.md
+++ b/openspec/changes/archive/2026-04-16-add-user-auth/tasks.md
@@ -69,7 +69,7 @@
### Green
-- [x] 4.5 Create `src/token-validation.ts` with `validateToken(config)` that sends `GET {projectUrl}/current` with the appropriate auth header and returns a boolean
+- [x] 4.5 Create `src/token-validation.ts` with `validateToken(config)` that sends `GET {baseUrl}/current` with the appropriate auth header and returns a boolean
### Refactor
diff --git a/openspec/changes/archive/2026-04-21-add-mcp-config-step-to-application-skill/specs/formio-application-skill/spec.md b/openspec/changes/archive/2026-04-21-add-mcp-config-step-to-application-skill/specs/formio-application-skill/spec.md
index c4bf396..639537a 100644
--- a/openspec/changes/archive/2026-04-21-add-mcp-config-step-to-application-skill/specs/formio-application-skill/spec.md
+++ b/openspec/changes/archive/2026-04-21-add-mcp-config-step-to-application-skill/specs/formio-application-skill/spec.md
@@ -10,7 +10,7 @@ The MCP server SHALL register a new tool named `authenticate` at `packages/mcp-s
- Returns a JSON-serialized text content block whose payload is `{ authenticated: boolean, cached: boolean, projectUrl: string, userEmail?: string }`. The JWT MUST NOT appear in the return payload.
- Reads the project URL from the server's `FormioConfig`. The agent does NOT pass a URL.
- `cached: true` when the JWT was already present before the call; `cached: false` when the call triggered a fresh login.
-- `userEmail` is best-effort — populated from a `GET {projectUrl}/current` call when the returned submission has an email field. Any error fetching the current user is swallowed; the field is simply omitted.
+- `userEmail` is best-effort — populated from a `GET {baseUrl}/current` call when the returned submission has an email field. Any error fetching the current user is swallowed; the field is simply omitted.
- Is the tool Step 4 of `formio-application` calls explicitly to trigger authentication. It is also available to any other skill that wants to pre-authenticate before a sensitive sequence.
#### Scenario: Tool is registered and callable
@@ -33,7 +33,7 @@ The MCP server SHALL register a new tool named `authenticate` at `packages/mcp-s
#### Scenario: Current-user email included when available
-- **WHEN** a successful `authenticate` call finishes and `GET {projectUrl}/current` returns a submission with an email field
+- **WHEN** a successful `authenticate` call finishes and `GET {baseUrl}/current` returns a submission with an email field
- **THEN** the payload contains `userEmail: `
#### Scenario: Current-user fetch failure is swallowed
diff --git a/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/.openspec.yaml b/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/.openspec.yaml
new file mode 100644
index 0000000..a5aaf30
--- /dev/null
+++ b/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-tdd
+created: 2026-05-22
diff --git a/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/design.md b/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/design.md
new file mode 100644
index 0000000..bb2ca26
--- /dev/null
+++ b/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/design.md
@@ -0,0 +1,150 @@
+## Context
+
+`plugin/skills/` already hosts source-derived skills for REST endpoints (`formio-api`), app orchestration (`formio-application`), schema planning (`formio-resource-planner`), and framework implementors (`formio-angular`). What is missing is a skill for the runtime JavaScript surface: the `Formio` SDK (CRUD, auth, projects, files, plugins) and the `Utils` namespace (formula evaluation, condition logic, component traversal, masks, sanitization, date helpers, i18n, JSONLogic).
+
+The canonical sources live outside this repo at the Form.io source repository (kept outside this repo at authoring time):
+
+- SDK core: `packages/core/src/sdk/Formio.ts`, `Plugins.ts`, `index.ts`
+- SDK renderer extensions: `packages/formio.js/src/Formio.js` (re-exports + adds icon/template/CDN/license/plugin glue)
+- Utils core: `packages/core/src/utils/` (`Evaluator.ts`, `conditions.ts`, `logic.ts`, `formUtil/`, `jsonlogic/`, `mask.ts`, `sanitize.ts`, `date.ts`, `dom.ts`, `i18n.ts`, `jwtDecode.ts`, `unwind.ts`, `utils.ts`)
+- Utils renderer extensions: `packages/formio.js/src/utils/` (`Evaluator.js`, `formUtils.js`, `builder.js`, `calendarUtils.js`, `ChoicesWrapper.js`, `conditionOperators/`, `i18n.js`, `utils.js`, `index.js`)
+
+Existing online documentation for these surfaces is known to drift from source; the skill MUST be derived from the source tree, not from `help.form.io` or third-party blog posts.
+
+The skill must fit the established library conventions:
+
+- Single activatable `SKILL.md` with three-clause description (capability / "Use when…" / "Not for: …").
+- One reference document per capability group under `references/`, no YAML frontmatter on references.
+- Per-reference `## MCP Tool Preference` block where applicable (CRUD methods that overlap `form_*` / `project_*` / `role_*` tools should defer to MCP first).
+- Strict terminology: `baseUrl` → `FORMIO_BASE_URL`; `projectUrl` → `FORMIO_PROJECT_URL`.
+- Validator coverage in `packages/mcp-server/src/skills-validator.ts` with Vitest enforcement under `pnpm test`.
+
+## Goals / Non-Goals
+
+**Goals:**
+
+- Teach Claude the actual runtime API surface of `@formio/js` and `@formio/js/utils` as it exists in the Form.io source HEAD, with executable examples.
+- Make canonical import statements (`import { Formio } from '@formio/js'` and `import { Utils } from '@formio/js/utils'`) non-negotiable — examples MUST use them; deep imports (`@formio/core`, `@formio/js/lib/...`) MUST NOT appear.
+- Make Hosted-vs-SaaS URL configuration the first thing the skill asserts. Every code example MUST be preceded (in its reference doc) by an explicit `setBaseUrl` + `setProjectUrl` pair appropriate to the environment.
+- Provide enough worked examples that an agent can implement common flows (login, list submissions, create form, upload file, evaluate condition, traverse components, run JSONLogic) without reading source.
+- Enforce skill structure mechanically via the existing validator infrastructure.
+
+**Non-Goals:**
+
+- Vendoring or building against the Form.io source tree at runtime — source is consulted at authoring time only.
+- Documenting the Form.io REST endpoints themselves — that is `formio-api`'s job. References cross-link there for endpoint shape.
+- Documenting Angular/React framework wrappers — those are owned by `formio-angular` and future framework skills.
+- Eval harness in the first iteration — add later under `plugin/skills/formio-sdk/evals/` once the skill stabilizes.
+
+## Decisions
+
+### Decision 1: Single skill, multiple references — NOT a per-method skill
+
+Adopt the `formio-api` shape: one activatable `SKILL.md` plus reference documents under `references/`. Alternative considered: separate `formio-sdk` and `formio-utils` skills. Rejected because the SDK and Utils are imported from the same package and agents routinely need both in the same edit; splitting forces double-activation and risks divergent guidance on imports / URL config.
+
+### Decision 2: Reference layout — capability-grouped, source-anchored
+
+References will be grouped by user-facing capability, not by source file. Proposed initial set (subject to confirmation while reading source):
+
+SDK references:
+
+- `references/setup.md` — `setBaseUrl`, `setProjectUrl`, Hosted vs SaaS, token storage, plugins registration
+- `references/auth.md` — `Formio.login`, `Formio.logout`, `Formio.currentUser`, `Formio.ssoInit`, JWT handling, OAuth helpers
+- `references/forms.md` — form CRUD via `new Formio(url).loadForm()/saveForm()/deleteForm()`, `loadForms`
+- `references/submissions.md` — submission CRUD, `loadSubmissions` with query, `availableActions`
+- `references/projects.md` — `loadProject`, `saveProject`, project-level helpers
+- `references/roles.md` — `loadRoles`, role assignment
+- `references/files.md` — `Formio.uploadFile`, `downloadFile`, provider config
+- `references/plugins.md` — `Formio.registerPlugin`, plugin lifecycle hooks (`preRequest`, `request`, `wrapRequestPromise`, etc.)
+- `references/rendering.md` — VanillaJS form rendering via `Formio.createForm(element, formSrc, options)`; covers prefill, events (`change`, `submit`, `error`, `nextPage`, `prevPage`), wizard, builder (`Formio.builder`), PDF (`Formio.createForm` with PDF source), read-only mode, custom templates/icons, and offline/local-JSON form sources. Behavior coverage is drawn from `formio.github.io/formio.js/app/examples` but ALL examples in this reference use ESM `import { Formio } from '@formio/js'` — never `` inside a fenced block
+- **THEN** `validateFormioSdkSkill` SHALL emit a `formio_sdk.forbidden_script_tag` issue
diff --git a/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/tasks.md b/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/tasks.md
new file mode 100644
index 0000000..c78b21d
--- /dev/null
+++ b/openspec/changes/archive/2026-05-22-add-formio-sdk-skill/tasks.md
@@ -0,0 +1,206 @@
+## 1. Validator scaffold (`validateFormioSdkSkill`)
+
+
+### Red
+
+- [x] 1.1 Write failing Vitest test: `validateFormioSdkSkill` exists, is exported from `packages/mcp-server/src/skills-validator.ts`, and returns `[]` when `plugin/skills/formio-sdk/` is absent from a temp library fixture
+- [x] 1.2 Write failing Vitest test: when `plugin/skills/formio-sdk/` exists but `SKILL.md` is absent, `validateFormioSdkSkill` emits exactly one issue with `category: "formio_sdk"` and `rule: "skill_missing"`
+- [x] 1.3 Write failing Vitest test: `validateLibrary` invokes `validateFormioSdkSkill` and propagates its issues into the aggregated result (assert the new issue surfaces through the public entry point)
+
+### Green
+
+- [x] 1.4 Add a `validateFormioSdkSkill(libraryDir)` export with the minimal logic needed to pass 1.1–1.2 (existence check + `skill_missing` emission)
+- [x] 1.5 Wire `validateFormioSdkSkill` into `validateLibrary` so 1.3 passes
+
+### Refactor
+
+- [x] 1.6 Review implementation and refactor as needed
+
+## 2. Frontmatter + three-clause description rules
+
+
+### Red
+
+- [x] 2.1 Write failing test: empty `SKILL.md` (no YAML frontmatter) emits `formio_sdk.frontmatter_missing`
+- [x] 2.2 Write failing test: frontmatter with `name: formio-sdk` but a `description` that lacks `Use when the user asks to` emits `formio_sdk.description_clause` with `clause: "trigger"`
+- [x] 2.3 Write failing test: description with the trigger clause but no `Not for:` clause emits `formio_sdk.description_clause` with `clause: "negative"`
+- [x] 2.4 Write failing test: `Not for:` clause that omits the literal `formio-api` emits `formio_sdk.description_clause` with `clause: "negative"` whose payload names `formio-api`
+- [x] 2.5 Write failing test: description with all three clauses and all four sibling skill names returns no `description_clause` issues
+
+### Green
+
+- [x] 2.6 Implement frontmatter parsing and the three-clause description checks against the failing tests
+- [x] 2.7 Implement the sibling-name check covering `formio-api`, `formio-application`, `formio-resource-planner`, and `formio-angular`
+
+### Refactor
+
+- [x] 2.8 Review implementation and refactor as needed
+
+## 3. Canonical-import + forbidden-import enforcement
+
+
+### Red
+
+- [x] 3.1 Write failing test: a `SKILL.md` that lacks `import { Formio } from '@formio/js'` emits `formio_sdk.canonical_import_missing` with `which: "sdk"`
+- [x] 3.2 Write failing test: a `SKILL.md` that lacks `import { Utils } from '@formio/js/utils'` emits `formio_sdk.canonical_import_missing` with `which: "utils"`
+- [x] 3.3 Write failing test: a reference doc with a fenced block containing `import { Formio } from '@formio/core'` emits `formio_sdk.forbidden_import` with `import_path: "@formio/core"`
+- [x] 3.4 Write failing test: a fenced block containing `import x from '@formio/js/lib/Formio'` emits `formio_sdk.forbidden_import` with `import_path` beginning `@formio/js/lib/`
+- [x] 3.5 Write failing test: a fenced JS block containing `const { Formio } = require('@formio/js');` emits `formio_sdk.forbidden_import` with `import_path: "@formio/js"`
+- [x] 3.6 Write failing test: prose mention of `@formio/core` OUTSIDE any code fence emits zero `formio_sdk.forbidden_import` issues
+- [x] 3.7 Write failing test: a fenced block containing `` emits `formio_sdk.forbidden_script_tag`
+- [x] 3.8 Write failing test: prose mention of `` inside a fenced block
+- **THEN** `validateFormioSdkSkill` SHALL emit a `formio_sdk.forbidden_script_tag` issue
diff --git a/openspec/specs/token-validation/spec.md b/openspec/specs/token-validation/spec.md
index d81f3cc..66ba2d2 100644
--- a/openspec/specs/token-validation/spec.md
+++ b/openspec/specs/token-validation/spec.md
@@ -13,20 +13,20 @@ On startup, the MCP server SHALL validate any available token (cached JWT or API
#### Scenario: Valid API key on startup
- **WHEN** the server starts with `FORMIO_API_KEY` set to a valid key
-- **THEN** `GET {projectUrl}/current` with `x-token` header returns 200
+- **THEN** `GET {baseUrl}/current` with `x-token` header returns 200
- **AND** the server proceeds normally
#### Scenario: Expired JWT triggers login flow
- **WHEN** the server starts with a cached JWT that has expired
-- **THEN** `GET {projectUrl}/current` returns 401
+- **THEN** `GET {baseUrl}/current` returns 401
- **AND** the cached token is cleared
- **AND** the login flow is triggered
#### Scenario: Invalid API key fails with error
- **WHEN** the server starts with `FORMIO_API_KEY` set to an invalid key
-- **THEN** `GET {projectUrl}/current` returns 401
+- **THEN** `GET {baseUrl}/current` returns 401
- **AND** the server throws an error indicating the API key is invalid
#### Scenario: No token and no API key triggers login flow
diff --git a/packages/mcp-server/src/__tests__/formio-angular-layout.test.ts b/packages/mcp-server/src/__tests__/formio-angular-layout.test.ts
deleted file mode 100644
index eb4d6ac..0000000
--- a/packages/mcp-server/src/__tests__/formio-angular-layout.test.ts
+++ /dev/null
@@ -1,273 +0,0 @@
-import fs from 'node:fs';
-import path from 'node:path';
-import matter from 'gray-matter';
-import { describe, expect, it } from 'vitest';
-
-const REPO_ROOT = path.resolve(__dirname, '../../../..');
-const SKILLS_DIR = path.join(REPO_ROOT, 'plugin/skills');
-
-const PARENT_SKILL = path.join(SKILLS_DIR, 'formio-angular/SKILL.md');
-const PARENT_SETUP = path.join(SKILLS_DIR, 'formio-angular/SETUP.md');
-const PARENT_CONFIG = path.join(SKILLS_DIR, 'formio-angular/CONFIG.md');
-const PARENT_AUTH = path.join(SKILLS_DIR, 'formio-angular/AUTH.md');
-const SUB_SKILL = path.join(SKILLS_DIR, 'formio-angular/resources/SKILL.md');
-const SUB_EVALS = path.join(SKILLS_DIR, 'formio-angular/resources/evals');
-
-const OLD_SKILL_DIR = path.join(SKILLS_DIR, 'formio-resource-angular');
-
-// Trigger-surface assertions for formio-angular + formio-angular-resources live
-// in formio-application-layout.test.ts now — after the `add-formio-application-orchestrator`
-// change demoted these two skills to framework-explicit triggers only.
-// This file retains only the infrastructure assertions (file layout, symlinks,
-// sibling-doc content) that don't depend on trigger-surface wording.
-
-function exists(p: string): boolean {
- try {
- fs.lstatSync(p);
- return true;
- } catch {
- return false;
- }
-}
-
-function readFrontmatter(file: string): Record {
- const raw = fs.readFileSync(file, 'utf8');
- return matter(raw).data as Record;
-}
-
-function readBody(file: string): string {
- const raw = fs.readFileSync(file, 'utf8');
- return matter(raw).content;
-}
-
-describe('formio-angular skill layout', () => {
- it('parent SKILL.md and sibling reference docs exist, old resource-angular dir removed', () => {
- expect(exists(PARENT_SKILL)).toBe(true);
- expect(exists(PARENT_SETUP)).toBe(true);
- expect(exists(PARENT_CONFIG)).toBe(true);
- expect(exists(PARENT_AUTH)).toBe(true);
- expect(exists(SUB_SKILL)).toBe(true);
- expect(exists(OLD_SKILL_DIR)).toBe(false);
- });
-});
-
-describe('formio-angular sub-skill frontmatter', () => {
- it('sub-skill name is formio-angular-resources', () => {
- const fm = readFrontmatter(SUB_SKILL);
- expect(fm.name).toBe('formio-angular-resources');
- });
-
- it('sub-skill description contains Not-for clause naming parent formio-angular', () => {
- const fm = readFrontmatter(SUB_SKILL);
- const description = String(fm.description ?? '');
- expect(description).toMatch(/not for:/i);
- expect(description).toContain('formio-angular');
- });
-});
-
-describe('formio-angular parent frontmatter', () => {
- it('parent name and description are set', () => {
- const fm = readFrontmatter(PARENT_SKILL);
- expect(fm.name).toBe('formio-angular');
- expect(String(fm.description ?? '').length).toBeGreaterThan(0);
- });
-
- it('parent description negatives point at formio-angular-resources', () => {
- const fm = readFrontmatter(PARENT_SKILL);
- const description = String(fm.description ?? '');
- expect(description).toContain('formio-angular-resources');
- expect(description).toMatch(/not for:/i);
- });
-
- it('parent body names the four phases in SETUP, CONFIG, AUTH, Resources order', () => {
- const body = readBody(PARENT_SKILL);
- const setupIdx = body.search(/\bSETUP\b/);
- const configIdx = body.search(/\bCONFIG\b/);
- const authIdx = body.search(/\bAUTH\b/);
- const resourcesIdx = body.search(/formio-angular-resources/);
- expect(setupIdx).toBeGreaterThan(-1);
- expect(configIdx).toBeGreaterThan(-1);
- expect(authIdx).toBeGreaterThan(-1);
- expect(resourcesIdx).toBeGreaterThan(-1);
- expect(setupIdx).toBeLessThan(configIdx);
- expect(configIdx).toBeLessThan(authIdx);
- expect(authIdx).toBeLessThan(resourcesIdx);
- });
-});
-
-describe('formio-angular SETUP.md', () => {
- it('has no YAML frontmatter', () => {
- const raw = fs.readFileSync(PARENT_SETUP, 'utf8');
- expect(raw.startsWith('---')).toBe(false);
- });
-
- it('captures Project URL and Base URL', () => {
- const raw = fs.readFileSync(PARENT_SETUP, 'utf8');
- expect(raw).toMatch(/Project URL/);
- expect(raw).toMatch(/Base URL/);
- });
-
- it('names FORMIO_PROJECT_URL and FORMIO_BASE_URL explicitly', () => {
- const raw = fs.readFileSync(PARENT_SETUP, 'utf8');
- expect(raw).toContain('FORMIO_PROJECT_URL');
- expect(raw).toContain('FORMIO_BASE_URL');
- });
-
- it('batches the URL questions into a single AskUserQuestion', () => {
- const raw = fs.readFileSync(PARENT_SETUP, 'utf8');
- expect(raw).toMatch(/AskUserQuestion/);
- expect(raw).toMatch(/(single|one|batch)/i);
- });
-});
-
-describe('formio-angular CONFIG.md', () => {
- it('has no frontmatter', () => {
- const raw = fs.readFileSync(PARENT_CONFIG, 'utf8');
- expect(raw.startsWith('---')).toBe(false);
- });
-
- it('references canonical external URLs', () => {
- const raw = fs.readFileSync(PARENT_CONFIG, 'utf8');
- expect(raw).toContain('https://help.form.io/developers/introduction/application');
- expect(raw).toContain('https://github.com/formio/angular-demo/blob/master/src/app/config.ts');
- expect(raw).toContain(
- 'https://github.com/formio/angular-demo/blob/master/src/app/app.module.ts'
- );
- });
-
- it('contains a config.ts code template with FormioAppConfig', () => {
- const raw = fs.readFileSync(PARENT_CONFIG, 'utf8');
- expect(raw).toContain('AppConfig');
- expect(raw).toContain('FormioAppConfig');
- expect(raw).toContain('appUrl');
- expect(raw).toContain('apiUrl');
- });
-
- it('guides AppModule wiring with provider token and FormioModule import', () => {
- const raw = fs.readFileSync(PARENT_CONFIG, 'utf8');
- expect(raw).toContain('provide: FormioAppConfig');
- expect(raw).toContain('useValue: AppConfig');
- expect(raw).toContain('FormioModule');
- expect(raw).toContain('@formio/angular');
- });
-
- it('describes preview-then-approve gate', () => {
- const raw = fs.readFileSync(PARENT_CONFIG, 'utf8');
- expect(raw).toMatch(/preview/i);
- expect(raw).toMatch(/approv/i);
- });
-
- it('describes skip-if-already-wired detection', () => {
- const raw = fs.readFileSync(PARENT_CONFIG, 'utf8');
- expect(raw).toMatch(/skip/i);
- expect(raw).toMatch(/(already|existing)/i);
- });
-});
-
-describe('formio-angular AUTH.md', () => {
- it('has no frontmatter', () => {
- const raw = fs.readFileSync(PARENT_AUTH, 'utf8');
- expect(raw.startsWith('---')).toBe(false);
- });
-
- it('references canonical external URLs', () => {
- const raw = fs.readFileSync(PARENT_AUTH, 'utf8');
- expect(raw).toContain(
- 'https://help.form.io/developers/introduction/application#user-authentication'
- );
- expect(raw).toContain(
- 'https://github.com/formio/angular-demo/blob/master/src/app/auth/auth.module.ts'
- );
- expect(raw).toContain(
- 'https://github.com/formio/angular-demo/blob/master/src/app/app.module.ts#L71'
- );
- });
-
- it('documents template.json extraction rules', () => {
- const raw = fs.readFileSync(PARENT_AUTH, 'utf8');
- expect(raw).toMatch(/template\.json/);
- expect(raw).toMatch(/user resource/i);
- expect(raw).toMatch(/login form/i);
- expect(raw).toMatch(/register form/i);
- expect(raw).toMatch(/role/i);
- });
-
- it('contains an auth.module.ts template using FormioAuthConfig', () => {
- const raw = fs.readFileSync(PARENT_AUTH, 'utf8');
- expect(raw).toContain('FormioAuthConfig');
- expect(raw).toContain('@formio/angular/auth');
- });
-
- it('documents no-template fallback TODO pointing at api skill references', () => {
- const raw = fs.readFileSync(PARENT_AUTH, 'utf8');
- expect(raw).toMatch(/TODO/);
- expect(raw).toContain('runtime-auth');
- expect(raw).toContain('platform-auth');
- });
-
- it('describes preview gate and skip-if-already-wired', () => {
- const raw = fs.readFileSync(PARENT_AUTH, 'utf8');
- expect(raw).toMatch(/preview/i);
- expect(raw).toMatch(/approv/i);
- expect(raw).toMatch(/skip/i);
- expect(raw).toMatch(/AuthModule/);
- });
-});
-
-describe('formio-angular eval-artifact paths', () => {
- it('evals dir contains no "formio-resource-angular" references', () => {
- const files = ['grade.py', 'README.md', 'evals.json'];
- for (const f of files) {
- const raw = fs.readFileSync(path.join(SUB_EVALS, f), 'utf8');
- expect(raw, `${f} must not reference old skill name`).not.toMatch(/formio-resource-angular/);
- }
- });
-
- it('grade.py references the renamed .eval-artifacts path', () => {
- const raw = fs.readFileSync(path.join(SUB_EVALS, 'grade.py'), 'utf8');
- expect(raw).toContain('formio-angular-resources');
- });
-});
-
-describe('repo-wide skill-name references', () => {
- it('CLAUDE.md names formio-angular and has no formio-resource-angular references', () => {
- const raw = fs.readFileSync(path.join(REPO_ROOT, 'CLAUDE.md'), 'utf8');
- expect(raw).toContain('formio-angular');
- expect(raw).not.toMatch(/formio-resource-angular/);
- expect(raw).toMatch(/skills\/formio-angular\/resources/);
- });
-
- it('formio-resource-planner skill has no formio-resource-angular references', () => {
- const plannerDir = path.join(SKILLS_DIR, 'formio-resource-planner');
- const stack = [plannerDir];
- const offenders: string[] = [];
- while (stack.length > 0) {
- const current = stack.pop()!;
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
- const full = path.join(current, entry.name);
- if (entry.isDirectory()) {
- stack.push(full);
- continue;
- }
- if (!entry.isFile()) continue;
- if (!/\.(md|json|py|ts|txt)$/.test(entry.name)) continue;
- const raw = fs.readFileSync(full, 'utf8');
- if (raw.includes('formio-resource-angular')) offenders.push(full);
- }
- }
- expect(offenders, `files still reference old skill name: ${offenders.join(', ')}`).toEqual([]);
- });
-});
-
-describe('formio-angular SKILL.md files parse as valid markdown+frontmatter', () => {
- it('both parent and sub-skill parse cleanly', () => {
- for (const file of [PARENT_SKILL, SUB_SKILL]) {
- const raw = fs.readFileSync(file, 'utf8');
- const parsed = matter(raw);
- expect(parsed.data, `${file} must have frontmatter`).toBeDefined();
- expect(typeof parsed.data.name).toBe('string');
- expect(typeof parsed.data.description).toBe('string');
- expect(parsed.content.length).toBeGreaterThan(0);
- }
- });
-});
diff --git a/packages/mcp-server/src/__tests__/formio-application-layout.test.ts b/packages/mcp-server/src/__tests__/formio-application-layout.test.ts
deleted file mode 100644
index d615d75..0000000
--- a/packages/mcp-server/src/__tests__/formio-application-layout.test.ts
+++ /dev/null
@@ -1,475 +0,0 @@
-import fs from 'node:fs';
-import path from 'node:path';
-import matter from 'gray-matter';
-import { describe, expect, it } from 'vitest';
-
-const REPO_ROOT = path.resolve(__dirname, '../../../..');
-const SKILLS_DIR = path.join(REPO_ROOT, 'plugin/skills');
-
-const APP_SKILL = path.join(SKILLS_DIR, 'formio-application/SKILL.md');
-const APP_INTENT = path.join(SKILLS_DIR, 'formio-application/INTENT.md');
-const APP_DEPLOYMENT = path.join(SKILLS_DIR, 'formio-application/DEPLOYMENT.md');
-const APP_MCP_CONFIG = path.join(SKILLS_DIR, 'formio-application/MCP_CONFIG.md');
-const APP_IMPORT = path.join(SKILLS_DIR, 'formio-application/IMPORT.md');
-const APP_FRAMEWORK = path.join(SKILLS_DIR, 'formio-application/FRAMEWORK.md');
-
-const ANGULAR_SKILL = path.join(SKILLS_DIR, 'formio-angular/SKILL.md');
-const ANGULAR_SUB_SKILL = path.join(SKILLS_DIR, 'formio-angular/resources/SKILL.md');
-const PLANNER_SKILL = path.join(SKILLS_DIR, 'formio-resource-planner/SKILL.md');
-
-// Plain-language build-new triggers formio-application MUST claim.
-const APP_BUILD_NEW_TRIGGERS = ['build me an app', 'create a crm', 'i need a tool to track'];
-// Plain-language extend triggers formio-application MUST claim.
-const APP_EXTEND_TRIGGERS = ['also track', 'add a way to see'];
-
-// Generic phrases formio-angular MUST NOT contain (they now belong to formio-application).
-const ANGULAR_FORBIDDEN_GENERIC = [
- 'build me an app',
- 'build me a tool',
- 'spin up an app',
- 'i need a tool to track',
- 'task manager',
- 'help desk',
- 'crm',
- 'booking system',
-];
-
-// Angular-explicit triggers formio-angular MUST claim.
-const ANGULAR_REQUIRED_EXPLICIT = ['build it in angular', 'angular front-end', 'use angular'];
-
-// Generic extend phrases formio-angular-resources MUST NOT contain.
-const ANGULAR_SUB_FORBIDDEN_GENERIC = [
- 'also track',
- 'also let',
- 'add a way to see',
- 'each x should have a list of y',
-];
-
-// Angular-explicit extend triggers formio-angular-resources MUST claim.
-const ANGULAR_SUB_REQUIRED_EXPLICIT = [
- 'add an angular module',
- 'regenerate the angular',
- 'in my angular app',
-];
-
-function exists(p: string): boolean {
- try {
- fs.lstatSync(p);
- return true;
- } catch {
- return false;
- }
-}
-
-function readFrontmatter(file: string): Record {
- const raw = fs.readFileSync(file, 'utf8');
- return matter(raw).data as Record;
-}
-
-function readBody(file: string): string {
- const raw = fs.readFileSync(file, 'utf8');
- return matter(raw).content;
-}
-
-function sliceStepSection(body: string, stepNumber: number): string {
- const startRe = new RegExp(`^#+\\s+Step ${stepNumber}\\b`, 'm');
- const startMatch = startRe.exec(body);
- if (!startMatch) return '';
- const start = startMatch.index;
- const afterStart = body.slice(start + startMatch[0].length);
- const endRe = new RegExp(`^#+\\s+Step ${stepNumber + 1}\\b`, 'm');
- const endMatch = endRe.exec(afterStart);
- const end = endMatch ? start + startMatch[0].length + endMatch.index : body.length;
- return body.slice(start, end);
-}
-
-describe('formio-application skill layout', () => {
- it('parent SKILL.md and sibling docs exist', () => {
- expect(exists(APP_SKILL)).toBe(true);
- expect(exists(APP_INTENT)).toBe(true);
- expect(exists(APP_DEPLOYMENT)).toBe(true);
- expect(exists(APP_IMPORT)).toBe(true);
- expect(exists(APP_FRAMEWORK)).toBe(true);
- });
-
- it('sibling docs have no YAML frontmatter', () => {
- for (const f of [APP_INTENT, APP_DEPLOYMENT, APP_IMPORT, APP_FRAMEWORK]) {
- const raw = fs.readFileSync(f, 'utf8');
- expect(raw.startsWith('---'), `${f} must not have frontmatter`).toBe(false);
- }
- });
-});
-
-describe('formio-application frontmatter', () => {
- it('name is formio-application', () => {
- const fm = readFrontmatter(APP_SKILL);
- expect(fm.name).toBe('formio-application');
- });
-
- it('description claims plain-language build-new triggers', () => {
- const fm = readFrontmatter(APP_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- for (const phrase of APP_BUILD_NEW_TRIGGERS) {
- expect(description, `must contain "${phrase}"`).toContain(phrase);
- }
- });
-
- it('description claims plain-language extend triggers', () => {
- const fm = readFrontmatter(APP_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- for (const phrase of APP_EXTEND_TRIGGERS) {
- expect(description, `must contain "${phrase}"`).toContain(phrase);
- }
- });
-
- it('description contains Not-for clauses naming sibling skills', () => {
- const fm = readFrontmatter(APP_SKILL);
- const description = String(fm.description ?? '');
- expect(description).toMatch(/not for:/i);
- expect(description).toContain('formio-angular');
- expect(description).toContain('formio-angular-resources');
- expect(description).toContain('formio-resource-planner');
- expect(description).toContain('formio-api');
- });
-});
-
-describe('formio-application body', () => {
- it('names the six steps in order via section headers', () => {
- const body = readBody(APP_SKILL);
- const intentIdx = body.search(/^#+\s+Step 1[^\n]*Intent/im);
- const planIdx = body.search(/^#+\s+Step 2[^\n]*Plan/im);
- const deploymentIdx = body.search(/^#+\s+Step 3[^\n]*Deployment/im);
- const mcpConfigIdx = body.search(/^#+\s+Step 4[^\n]*MCP Config/im);
- const importIdx = body.search(/^#+\s+Step 5[^\n]*Import/im);
- const frameworkIdx = body.search(/^#+\s+Step 6[^\n]*Framework/im);
- for (const [name, idx] of [
- ['Step 1 — Intent', intentIdx],
- ['Step 2 — Plan', planIdx],
- ['Step 3 — Deployment', deploymentIdx],
- ['Step 4 — MCP Config', mcpConfigIdx],
- ['Step 5 — Import', importIdx],
- ['Step 6 — Framework', frameworkIdx],
- ] as const) {
- expect(idx, `body must have "${name}" section header`).toBeGreaterThan(-1);
- }
- expect(intentIdx).toBeLessThan(planIdx);
- expect(planIdx).toBeLessThan(deploymentIdx);
- expect(deploymentIdx).toBeLessThan(mcpConfigIdx);
- expect(mcpConfigIdx).toBeLessThan(importIdx);
- expect(importIdx).toBeLessThan(frameworkIdx);
- });
-
- it('references the five sibling docs by relative link', () => {
- const body = readBody(APP_SKILL);
- expect(body).toContain('INTENT.md');
- expect(body).toContain('DEPLOYMENT.md');
- expect(body).toContain('MCP_CONFIG.md');
- expect(body).toContain('IMPORT.md');
- expect(body).toContain('FRAMEWORK.md');
- });
-
- it('description mentions .mcp.json and the restart pause', () => {
- const fm = readFrontmatter(APP_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- expect(description).toContain('.mcp.json');
- expect(description).toMatch(/(restart|reconnect)/);
- });
-
- it('Step 2 (Plan) invokes formio-resource-planner in both modes', () => {
- const body = readBody(APP_SKILL);
- const step2 = sliceStepSection(body, 2).toLowerCase();
- expect(step2.length, 'Step 2 section must exist').toBeGreaterThan(0);
- expect(step2).toContain('formio-resource-planner');
- expect(step2).toMatch(/delta/);
- expect(step2).toMatch(/(additive|merge)/);
- });
-
- it('Step 5 (Import) runs on both branches with implicit auth', () => {
- const body = readBody(APP_SKILL);
- const step5 = sliceStepSection(body, 5).toLowerCase();
- expect(step5.length, 'Step 5 section must exist').toBeGreaterThan(0);
- expect(step5).toContain('project_import');
- expect(step5).toMatch(/additive/);
- expect(step5).toMatch(/(implicit|first authenticated|portal.login)/);
- });
-
- it('Step 4 section documents halt + restart + modify-existing skip', () => {
- const body = readBody(APP_SKILL);
- const step4 = sliceStepSection(body, 4).toLowerCase();
- expect(step4.length, 'Step 4 section must exist').toBeGreaterThan(0);
- expect(step4).toContain('mcp_config.md');
- expect(step4).toMatch(/(halt|stop|pause)/);
- expect(step4).toMatch(/(restart|reconnect)/);
- expect(step4).toMatch(/(modify-existing|skipped)/);
- });
-});
-
-describe('INTENT.md content', () => {
- it('names AskUserQuestion and covers both branches', () => {
- const raw = fs.readFileSync(APP_INTENT, 'utf8');
- expect(raw).toContain('AskUserQuestion');
- expect(raw.toLowerCase()).toMatch(/build/);
- expect(raw.toLowerCase()).toMatch(/(modify|extend)/);
- });
-
- it('documents modify-existing skips only Deployment + MCP Config (still runs planner + import)', () => {
- const raw = fs.readFileSync(APP_INTENT, 'utf8').toLowerCase();
- expect(raw).toMatch(/skip/);
- expect(raw).toMatch(/(deployment|step 3)/);
- expect(raw).toMatch(/(mcp config|mcp_config|mcp-config|step 4)/);
- // Modify-existing MUST still mention planner + import
- expect(raw).toContain('formio-resource-planner');
- expect(raw).toContain('project_import');
- expect(raw).toMatch(/delta/);
- expect(raw).toMatch(/additive/);
- });
-});
-
-describe('DEPLOYMENT.md content', () => {
- it('batches the URL interview and names env-var terms', () => {
- const raw = fs.readFileSync(APP_DEPLOYMENT, 'utf8');
- expect(raw).toContain('AskUserQuestion');
- expect(raw.toLowerCase()).toMatch(/(batched|single|one)/);
- expect(raw).toContain('FORMIO_PROJECT_URL');
- expect(raw).toContain('FORMIO_BASE_URL');
- });
-
- it('contains plain-language URL descriptions with examples', () => {
- const raw = fs.readFileSync(APP_DEPLOYMENT, 'utf8');
- expect(raw).toMatch(/Project URL/);
- expect(raw).toMatch(/Base URL/);
- expect(raw).toMatch(/https:\/\/[\w.-]*form\.io/);
- });
-
- it('names the next consumer of captured URLs (Step 4 / MCP_CONFIG.md)', () => {
- const raw = fs.readFileSync(APP_DEPLOYMENT, 'utf8');
- expect(raw).toMatch(/(Step 4|MCP Config|MCP_CONFIG\.md)/);
- });
-});
-
-describe('IMPORT.md content', () => {
- it('covers import flow and error branches', () => {
- const raw = fs.readFileSync(APP_IMPORT, 'utf8');
- expect(raw).toContain('project_import');
- expect(raw.toLowerCase()).toMatch(/merge/);
- expect(raw.toLowerCase()).toMatch(/(auth|401|403)/);
- expect(raw.toLowerCase()).toMatch(/(not found|404)/);
- expect(raw.toLowerCase()).toMatch(/(validation|400)/);
- });
-
- it('describes browser login and headless fallback', () => {
- const raw = fs.readFileSync(APP_IMPORT, 'utf8');
- expect(raw.toLowerCase()).toMatch(/browser/);
- expect(raw.toLowerCase()).toMatch(/(headless|portal.login|print)/);
- });
-
- it('Step 5 describes implicit portal-login on first authenticated call', () => {
- const raw = fs.readFileSync(APP_IMPORT, 'utf8').toLowerCase();
- expect(raw).toMatch(/(implicit|first authenticated|portal.login|browser)/);
- });
-});
-
-describe('MCP_CONFIG.md content', () => {
- it('exists and has no frontmatter', () => {
- expect(exists(APP_MCP_CONFIG)).toBe(true);
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8');
- expect(raw.startsWith('---')).toBe(false);
- });
-
- it('names both env-var keys', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8');
- expect(raw).toContain('FORMIO_PROJECT_URL');
- expect(raw).toContain('FORMIO_BASE_URL');
- });
-
- it('documents the FORMIO_BASE_URL ↔ FORMIO_BASE_URL mapping', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8');
- expect(raw).toContain('FORMIO_BASE_URL');
- expect(raw).toContain('FORMIO_BASE_URL');
- expect(raw.toLowerCase()).toMatch(/(map|same concept|two names)/);
- });
-
- it('documents collision handling (preserve command/args, preserve other env, preserve other servers)', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8').toLowerCase();
- expect(raw).toMatch(/preserve/);
- expect(raw).toMatch(/command/);
- expect(raw).toMatch(/args/);
- expect(raw).toMatch(/(unrelated|other) (mcp|server)/);
- });
-
- it('documents npm-based default command and placeholder warning', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8');
- expect(raw).toContain('npx');
- expect(raw.toLowerCase()).toMatch(/@formio\/mcp/);
- expect(raw.toLowerCase()).toMatch(/(placeholder|aspirational|not.*published|until.*publish)/);
- });
-
- it('documents the approval gate (preview, approval, write, then restart)', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8').toLowerCase();
- expect(raw).toMatch(/preview/);
- expect(raw).toMatch(/approv/);
- expect(raw).toMatch(/(restart|reconnect)/);
- });
-
- it('documents the skip rule for matching existing entries', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8').toLowerCase();
- expect(raw).toMatch(/skip/);
- expect(raw).toMatch(/match/);
- });
-
- it('mentions both "restart Claude Code" and "/mcp" reconnect paths', () => {
- const raw = fs.readFileSync(APP_MCP_CONFIG, 'utf8');
- expect(raw.toLowerCase()).toMatch(/restart claude code/);
- expect(raw).toContain('/mcp');
- });
-});
-
-describe('FRAMEWORK.md content', () => {
- it('contains a registry table with Angular row', () => {
- const raw = fs.readFileSync(APP_FRAMEWORK, 'utf8');
- expect(raw).toMatch(/\| Framework \|/);
- expect(raw).toContain('formio-angular');
- expect(raw).toContain('formio-angular-resources');
- expect(raw.toLowerCase()).toMatch(/angular\.json/);
- });
-
- it('documents single-row silent routing and multi-row picker', () => {
- const raw = fs.readFileSync(APP_FRAMEWORK, 'utf8').toLowerCase();
- expect(raw).toMatch(/(silent|directly|without asking)/);
- expect(raw).toMatch(/(askuserquestion|picker|ask the user)/);
- });
-
- it('documents how to add a new framework row', () => {
- const raw = fs.readFileSync(APP_FRAMEWORK, 'utf8').toLowerCase();
- expect(raw).toMatch(/add/);
- expect(raw).toMatch(/(row|entry|new framework)/);
- });
-});
-
-describe('formio-angular demotion', () => {
- it('description drops generic build-an-app phrases', () => {
- const fm = readFrontmatter(ANGULAR_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- for (const phrase of ANGULAR_FORBIDDEN_GENERIC) {
- expect(description, `must not contain "${phrase}"`).not.toContain(phrase);
- }
- });
-
- it('description claims Angular-explicit triggers', () => {
- const fm = readFrontmatter(ANGULAR_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- for (const phrase of ANGULAR_REQUIRED_EXPLICIT) {
- expect(description, `must contain "${phrase}"`).toContain(phrase);
- }
- });
-
- it('description points at formio-application in Not-for clause', () => {
- const fm = readFrontmatter(ANGULAR_SKILL);
- const description = String(fm.description ?? '');
- expect(description).toContain('formio-application');
- expect(description).toMatch(/not for:/i);
- });
-});
-
-describe('formio-angular body post-demotion', () => {
- it('body does not describe an Inference phase', () => {
- const body = readBody(ANGULAR_SKILL);
- expect(body).not.toMatch(/Phase 0 — Inference/);
- expect(body).not.toMatch(/## Phase 0 — Inference/);
- });
-
- it('body documents SETUP handoff from formio-application', () => {
- const body = readBody(ANGULAR_SKILL);
- expect(body).toContain('formio-application');
- expect(body.toLowerCase()).toMatch(/handoff|handed/);
- });
-
- it("body states Import is not this skill's responsibility", () => {
- const body = readBody(ANGULAR_SKILL).toLowerCase();
- expect(body).toContain('project_import');
- expect(body).toMatch(/(not this skill|lives in formio-application|is formio-application)/);
- });
-});
-
-describe('formio-angular-resources demotion', () => {
- it('description drops generic extend phrases', () => {
- const fm = readFrontmatter(ANGULAR_SUB_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- for (const phrase of ANGULAR_SUB_FORBIDDEN_GENERIC) {
- expect(description, `must not contain "${phrase}"`).not.toContain(phrase);
- }
- });
-
- it('description claims Angular-explicit extend triggers', () => {
- const fm = readFrontmatter(ANGULAR_SUB_SKILL);
- const description = String(fm.description ?? '').toLowerCase();
- for (const phrase of ANGULAR_SUB_REQUIRED_EXPLICIT) {
- expect(description, `must contain "${phrase}"`).toContain(phrase);
- }
- });
-
- it('description points at formio-application in Not-for clause', () => {
- const fm = readFrontmatter(ANGULAR_SUB_SKILL);
- const description = String(fm.description ?? '');
- expect(description).toContain('formio-application');
- expect(description).toMatch(/not for:/i);
- });
-});
-
-describe('formio-resource-planner disk-write documentation', () => {
- it('Phase B section documents writing template.json to cwd', () => {
- const body = readBody(PLANNER_SKILL).toLowerCase();
- expect(body).toMatch(/template\.json/);
- expect(body).toMatch(/(write tool|cwd|working directory)/);
- expect(body).toMatch(/template-.*timestamp/);
- });
-
- it('Phase B guidance mentions both standalone and orchestrator invocation', () => {
- const body = readBody(PLANNER_SKILL).toLowerCase();
- expect(body).toMatch(/(standalone|directly)/);
- expect(body).toMatch(/formio-application/);
- });
-});
-
-describe('CLAUDE.md updates', () => {
- it('names formio-application as build-an-app entry point', () => {
- const raw = fs.readFileSync(path.join(REPO_ROOT, 'CLAUDE.md'), 'utf8');
- expect(raw).toContain('formio-application');
- expect(raw).toMatch(/skills\/formio-application/);
- });
-});
-
-describe('YAML frontmatter regression guard', () => {
- it('all SKILL.md files under formio-application and formio-angular parse cleanly', () => {
- const files = [APP_SKILL, ANGULAR_SKILL, ANGULAR_SUB_SKILL];
- for (const file of files) {
- const raw = fs.readFileSync(file, 'utf8');
- const parsed = matter(raw);
- expect(parsed.data, `${file} must have frontmatter`).toBeDefined();
- expect(typeof parsed.data.name).toBe('string');
- expect(typeof parsed.data.description).toBe('string');
- expect(parsed.content.length).toBeGreaterThan(0);
- }
- });
-});
-
-describe('trigger-surface non-overlap', () => {
- it('formio-application claims generic phrases that formio-angular does not', () => {
- const appDesc = String(readFrontmatter(APP_SKILL).description ?? '').toLowerCase();
- const angularDesc = String(readFrontmatter(ANGULAR_SKILL).description ?? '').toLowerCase();
- for (const phrase of APP_BUILD_NEW_TRIGGERS) {
- expect(appDesc).toContain(phrase);
- expect(angularDesc, `formio-angular must not claim "${phrase}"`).not.toContain(phrase);
- }
- });
-
- it('formio-angular claims framework-explicit phrases that formio-application does not', () => {
- const appDesc = String(readFrontmatter(APP_SKILL).description ?? '').toLowerCase();
- const angularDesc = String(readFrontmatter(ANGULAR_SKILL).description ?? '').toLowerCase();
- for (const phrase of ANGULAR_REQUIRED_EXPLICIT) {
- expect(angularDesc).toContain(phrase);
- }
- // formio-application should not claim the specific "build it in Angular" phrasing.
- expect(appDesc).not.toContain('build it in angular');
- });
-});
diff --git a/packages/mcp-server/src/__tests__/skills-library.test.ts b/packages/mcp-server/src/__tests__/skills-library.test.ts
deleted file mode 100644
index 9d0ebd2..0000000
--- a/packages/mcp-server/src/__tests__/skills-library.test.ts
+++ /dev/null
@@ -1,393 +0,0 @@
-import fs from 'node:fs';
-import os from 'node:os';
-import path from 'node:path';
-import { afterEach, beforeEach, describe, expect, it } from 'vitest';
-import {
- CANONICAL_AUTH_PARAGRAPH,
- MCP_PREFERENCE_FALLBACK_SENTENCE,
- MCP_PREFERENCE_HEADING,
- REFERENCES_DIRNAME,
- REQUIRED_REFERENCE_GROUPS,
- REQUIRED_REFERENCE_HEADINGS,
- ROUTER_DIR,
- SKILL_FILENAME,
- type ReferenceGroup,
- GROUP_SCOPE,
- validateLibrary,
- validateNoRandomIdSuffixes,
- validateReferenceContent,
- validateRequiredFiles,
- validateRouterLinks,
- validateRouterSkillContent,
-} from '../skills-validator.js';
-
-function makeReferenceBody(
- group: ReferenceGroup,
- overrides: { omit?: string[]; extra?: string; rootUrlBlock?: string } = {}
-) {
- const omit = new Set(overrides.omit ?? []);
- const scope = GROUP_SCOPE[group];
- const rootUrl = scope === 'platform' ? '${FORMIO_BASE_URL}' : '${FORMIO_PROJECT_URL}';
- const sections: string[] = [];
- if (!omit.has('Overview')) sections.push('## Overview\n\nExample overview.');
- if (!omit.has('Root URL'))
- sections.push(
- overrides.rootUrlBlock ?? `## Root URL\n\nAll endpoints rooted at \`${rootUrl}\`.`
- );
- if (!omit.has('Authentication')) {
- // server-status skips canonical auth paragraph
- if (group === 'server-status') {
- sections.push('## Authentication\n\nNo auth required — public health endpoints.');
- } else {
- sections.push(`## Authentication\n\n${CANONICAL_AUTH_PARAGRAPH}`);
- }
- }
- if (!omit.has('MCP Tool Preference')) {
- sections.push(`${MCP_PREFERENCE_HEADING}\n\n${MCP_PREFERENCE_FALLBACK_SENTENCE}`);
- }
- if (!omit.has('Endpoints')) {
- const path = scope === 'pdf' ? `${rootUrl}/pdf-proxy/file` : `${rootUrl}/form`;
- sections.push(`## Endpoints\n\n### GET ${path}\n\nList.`);
- }
- return sections.join('\n\n') + (overrides.extra ? `\n\n${overrides.extra}` : '') + '\n';
-}
-
-function makeRouterSource(
- overrides: {
- linksToAllGroups?: boolean;
- extraHeadings?: string;
- descriptionOverride?: string;
- } = {}
-) {
- const description =
- overrides.descriptionOverride ??
- 'Comprehensive Form.io API reference. Use when the user asks about any Form.io REST endpoint. Not for: building an app (see formio-application).';
- const links =
- (overrides.linksToAllGroups ?? true)
- ? REQUIRED_REFERENCE_GROUPS.map((g) => `- [${g}](./${REFERENCES_DIRNAME}/${g}.md)`).join('\n')
- : '';
- return `---
-name: formio-api
-description: "${description.replace(/"/g, '\\"')}"
----
-
-# Form.io API
-
-${links}
-
-${overrides.extraHeadings ?? ''}
-`;
-}
-
-function writeLibrary(root: string) {
- fs.mkdirSync(path.join(root, ROUTER_DIR, REFERENCES_DIRNAME), { recursive: true });
- fs.writeFileSync(path.join(root, ROUTER_DIR, SKILL_FILENAME), makeRouterSource());
- for (const group of REQUIRED_REFERENCE_GROUPS) {
- fs.writeFileSync(
- path.join(root, ROUTER_DIR, REFERENCES_DIRNAME, `${group}.md`),
- makeReferenceBody(group)
- );
- }
-}
-
-describe('skills-validator — required files', () => {
- let tmpDir: string;
-
- beforeEach(() => {
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'skills-lib-'));
- });
- afterEach(() => {
- fs.rmSync(tmpDir, { recursive: true, force: true });
- });
-
- it('passes when router SKILL.md and all 17 reference files exist', () => {
- writeLibrary(tmpDir);
- expect(validateRequiredFiles(tmpDir)).toEqual([]);
- });
-
- it('fails when router SKILL.md is missing', () => {
- writeLibrary(tmpDir);
- fs.rmSync(path.join(tmpDir, ROUTER_DIR, SKILL_FILENAME));
- const issues = validateRequiredFiles(tmpDir);
- expect(
- issues.some((i) => i.rule === 'library.required_file' && i.message.includes('router'))
- ).toBe(true);
- });
-
- it('fails when a required reference file is missing', () => {
- writeLibrary(tmpDir);
- fs.rmSync(path.join(tmpDir, ROUTER_DIR, REFERENCES_DIRNAME, 'project-forms.md'));
- const issues = validateRequiredFiles(tmpDir);
- expect(issues.some((i) => i.message.includes('project-forms.md'))).toBe(true);
- });
-
- it('fails when a required reference file is empty', () => {
- writeLibrary(tmpDir);
- fs.writeFileSync(path.join(tmpDir, ROUTER_DIR, REFERENCES_DIRNAME, 'pdf-api.md'), '');
- const issues = validateRequiredFiles(tmpDir);
- expect(issues.some((i) => i.message.includes('pdf-api.md') && /empty/.test(i.message))).toBe(
- true
- );
- });
-});
-
-describe('skills-validator — router links', () => {
- let tmpDir: string;
- beforeEach(() => {
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'router-links-'));
- });
- afterEach(() => {
- fs.rmSync(tmpDir, { recursive: true, force: true });
- });
-
- it('passes when router links to every reference group', () => {
- writeLibrary(tmpDir);
- expect(validateRouterLinks(tmpDir)).toEqual([]);
- });
-
- it('fails when a required group link is absent from router', () => {
- writeLibrary(tmpDir);
- fs.writeFileSync(
- path.join(tmpDir, ROUTER_DIR, SKILL_FILENAME),
- makeRouterSource({ linksToAllGroups: false })
- );
- const issues = validateRouterLinks(tmpDir);
- expect(issues.some((i) => i.rule === 'index.missing_link')).toBe(true);
- });
-
- it('fails when router contains endpoint headings', () => {
- writeLibrary(tmpDir);
- fs.writeFileSync(
- path.join(tmpDir, ROUTER_DIR, SKILL_FILENAME),
- makeRouterSource({ extraHeadings: '### GET ${FORMIO_PROJECT_URL}/form\n\nNo.' })
- );
- const issues = validateRouterLinks(tmpDir);
- expect(issues.some((i) => i.rule === 'index.no_endpoint_docs')).toBe(true);
- });
-
- it('fails when router links to a non-existent reference file', () => {
- writeLibrary(tmpDir);
- const routerPath = path.join(tmpDir, ROUTER_DIR, SKILL_FILENAME);
- const orig = fs.readFileSync(routerPath, 'utf8');
- fs.writeFileSync(
- routerPath,
- `${orig}\n- [missing](./${REFERENCES_DIRNAME}/missing-group.md)\n`
- );
- const issues = validateRouterLinks(tmpDir);
- expect(issues.some((i) => i.rule === 'index.broken_link')).toBe(true);
- });
-});
-
-describe('skills-validator — router skill content', () => {
- it('passes with valid router frontmatter + trigger clauses', () => {
- const source = makeRouterSource();
- expect(validateRouterSkillContent('formio-api/SKILL.md', source)).toEqual([]);
- });
-
- it('fails when router description lacks "use when" trigger', () => {
- const source = makeRouterSource({
- descriptionOverride: 'Reference. Not for: building an app (see formio-application).',
- });
- const issues = validateRouterSkillContent('formio-api/SKILL.md', source);
- expect(issues.some((i) => i.rule === 'description.trigger_phrase')).toBe(true);
- });
-
- it('fails when router description lacks "not for:" clause', () => {
- const source = makeRouterSource({
- descriptionOverride: 'Reference. Use when the user asks about any Form.io endpoint.',
- });
- const issues = validateRouterSkillContent('formio-api/SKILL.md', source);
- expect(issues.some((i) => i.rule === 'description.negative_trigger')).toBe(true);
- });
-
- it('fails when router frontmatter.name is wrong', () => {
- const source = `---
-name: formio-wrong
-description: "Use when testing. Not for: production."
----
-body`;
- const issues = validateRouterSkillContent('formio-api/SKILL.md', source);
- expect(issues.some((i) => i.rule === 'frontmatter.name')).toBe(true);
- });
-
- it('fails when router has extra frontmatter keys', () => {
- const source = `---
-name: formio-api
-description: "Use when asking about Form.io. Not for: plan."
-scope: project
----
-body`;
- const issues = validateRouterSkillContent('formio-api/SKILL.md', source);
- expect(issues.some((i) => i.rule === 'frontmatter.keys')).toBe(true);
- });
-});
-
-describe('skills-validator — reference content', () => {
- it('passes with a well-formed reference doc (no frontmatter)', () => {
- const source = makeReferenceBody('project-forms');
- expect(validateReferenceContent('ref.md', source, 'project-forms')).toEqual([]);
- });
-
- it('fails when reference doc has frontmatter', () => {
- const source = `---\nname: something\n---\n\n${makeReferenceBody('project-forms')}`;
- const issues = validateReferenceContent('ref.md', source, 'project-forms');
- expect(issues.some((i) => i.rule === 'reference.no_frontmatter')).toBe(true);
- });
-
- it('fails when a required heading is missing', () => {
- const source = makeReferenceBody('project-forms', { omit: ['Overview'] });
- const issues = validateReferenceContent('ref.md', source, 'project-forms');
- expect(issues.some((i) => i.rule === 'headings.missing')).toBe(true);
- });
-
- it('fails when required headings are out of order', () => {
- const body = `## MCP Tool Preference\n\n${MCP_PREFERENCE_FALLBACK_SENTENCE}\n\n## Overview\n\ntext\n\n## Root URL\n\n\`\${FORMIO_PROJECT_URL}\`\n\n## Authentication\n\n${CANONICAL_AUTH_PARAGRAPH}\n\n## Endpoints\n\n### GET \${FORMIO_PROJECT_URL}/x\n\n.`;
- const issues = validateReferenceContent('ref.md', body, 'project-forms');
- expect(issues.some((i) => i.rule === 'headings.order')).toBe(true);
- });
-
- it('requires canonical auth paragraph (except server-status)', () => {
- const source = `## Overview\n\nx\n\n## Root URL\n\nx\n\n## Authentication\n\nnope\n\n## MCP Tool Preference\n\n${MCP_PREFERENCE_FALLBACK_SENTENCE}\n\n## Endpoints\n\n### GET x\n\n.`;
- const issues = validateReferenceContent('ref.md', source, 'project-forms');
- expect(issues.some((i) => i.rule === 'auth.canonical_paragraph')).toBe(true);
- });
-
- it('allows server-status without canonical auth paragraph', () => {
- const source = makeReferenceBody('server-status');
- const issues = validateReferenceContent('ref.md', source, 'server-status');
- expect(issues.every((i) => i.rule !== 'auth.canonical_paragraph')).toBe(true);
- });
-
- it('flags forbidden legacy-auth tokens', () => {
- const source = makeReferenceBody('project-forms', { extra: 'Include x-token header.' });
- const issues = validateReferenceContent('ref.md', source, 'project-forms');
- expect(issues.some((i) => i.rule === 'forbidden.legacy_auth')).toBe(true);
- });
-
- it('flags unresolved Postman project placeholder for project-scope refs', () => {
- const source = makeReferenceBody('project-forms', {
- extra: 'Path: {{baseUrl}}/{{projectName}}/form (needs substitution).',
- });
- const issues = validateReferenceContent('ref.md', source, 'project-forms');
- expect(issues.some((i) => i.rule === 'placeholder.project')).toBe(true);
- });
-
- it('flags bare {{baseUrl}}/ placeholder for platform-scope refs', () => {
- const source = makeReferenceBody('platform-auth', {
- extra: 'Path: {{baseUrl}}/user (needs substitution).',
- });
- const issues = validateReferenceContent('ref.md', source, 'platform-auth');
- expect(issues.some((i) => i.rule === 'placeholder.platform')).toBe(true);
- });
-
- it('flags pdf endpoints that do not begin with ${FORMIO_PROJECT_URL}/pdf-proxy', () => {
- const body = makeReferenceBody('pdf-api', {
- omit: ['Endpoints'],
- extra: '## Endpoints\n\n### GET ${FORMIO_PROJECT_URL}/file\n\nWrong path.',
- });
- // Re-inject endpoints after omit
- const source = body.replace('## Endpoints\n\n', '## Endpoints\n\n');
- const issues = validateReferenceContent('ref.md', source, 'pdf-api');
- expect(issues.some((i) => i.rule === 'pdf.proxy_path')).toBe(true);
- });
-
- it('flags terminology misuse of baseUrl for project endpoint', () => {
- const source = makeReferenceBody('project-forms', {
- extra: 'The baseUrl is the project endpoint here.',
- });
- const issues = validateReferenceContent('ref.md', source, 'project-forms');
- expect(issues.some((i) => i.rule === 'terminology.baseUrl_for_project')).toBe(true);
- });
-
- it('flags terminology misuse of projectUrl for platform endpoint', () => {
- const source = makeReferenceBody('platform-auth', {
- extra: 'The projectUrl is the platform deployment URL.',
- });
- const issues = validateReferenceContent('ref.md', source, 'platform-auth');
- expect(issues.some((i) => i.rule === 'terminology.projectUrl_for_platform')).toBe(true);
- });
-});
-
-describe('skills-validator — validateNoRandomIdSuffixes', () => {
- it('flags random-id integer suffix in example titles', () => {
- const body = '```json\n{"title": "My Form 42"}\n```';
- // code fences are stripped before scanning; regex runs against original body
- const issues = validateNoRandomIdSuffixes('ref.md', body);
- expect(issues.length).toBeGreaterThan(0);
- });
-
- it('flags random-id suffix in slug-style fields', () => {
- const body = '```json\n{"name": "myform-99"}\n```';
- const issues = validateNoRandomIdSuffixes('ref.md', body);
- expect(issues.length).toBeGreaterThan(0);
- });
-});
-
-describe('skills-validator — validateLibrary integration', () => {
- let tmpDir: string;
- beforeEach(() => {
- tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lib-integration-'));
- });
- afterEach(() => {
- fs.rmSync(tmpDir, { recursive: true, force: true });
- });
-
- it('passes on a well-formed library', () => {
- writeLibrary(tmpDir);
- expect(validateLibrary(tmpDir)).toEqual([]);
- });
-
- it('collects issues across required-files + router + every reference', () => {
- writeLibrary(tmpDir);
- // Break: delete a reference + corrupt router + corrupt one reference
- fs.rmSync(path.join(tmpDir, ROUTER_DIR, REFERENCES_DIRNAME, 'runtime-reports.md'));
- fs.writeFileSync(
- path.join(tmpDir, ROUTER_DIR, SKILL_FILENAME),
- makeRouterSource({ descriptionOverride: 'missing triggers' })
- );
- fs.writeFileSync(
- path.join(tmpDir, ROUTER_DIR, REFERENCES_DIRNAME, 'project-forms.md'),
- '## Overview\n\nmissing auth\n'
- );
- const issues = validateLibrary(tmpDir);
- const rules = new Set(issues.map((i) => i.rule));
- expect(rules.has('library.required_file')).toBe(true);
- expect(rules.has('description.trigger_phrase')).toBe(true);
- expect(rules.has('headings.missing')).toBe(true);
- });
-});
-
-describe('skills-validator — REQUIRED_REFERENCE_GROUPS invariants', () => {
- it('has 17 groups', () => {
- expect(REQUIRED_REFERENCE_GROUPS.length).toBe(17);
- });
-
- it('every group has a scope mapping', () => {
- for (const group of REQUIRED_REFERENCE_GROUPS) {
- expect(GROUP_SCOPE[group]).toBeDefined();
- }
- });
-
- it('REQUIRED_REFERENCE_HEADINGS includes MCP Tool Preference after Authentication', () => {
- const authIdx = REQUIRED_REFERENCE_HEADINGS.indexOf('## Authentication');
- const mcpIdx = REQUIRED_REFERENCE_HEADINGS.indexOf(MCP_PREFERENCE_HEADING);
- const endpointsIdx = REQUIRED_REFERENCE_HEADINGS.indexOf('## Endpoints');
- expect(authIdx).toBeGreaterThanOrEqual(0);
- expect(mcpIdx).toBe(authIdx + 1);
- expect(endpointsIdx).toBe(mcpIdx + 1);
- });
-});
-
-describe('skills-validator — real library on disk', () => {
- const REPO_ROOT = path.resolve(__dirname, '../../../..');
- const LIBRARY_DIR = path.join(REPO_ROOT, 'plugin/skills');
-
- it('real plugin/skills library validates cleanly', () => {
- const issues = validateLibrary(LIBRARY_DIR);
- // Print any issues for debugging
- if (issues.length > 0) {
- console.error('Library validation issues:', JSON.stringify(issues, null, 2));
- }
- expect(issues).toEqual([]);
- });
-});
diff --git a/packages/mcp-server/src/skills-validator.ts b/packages/mcp-server/src/skills-validator.ts
deleted file mode 100644
index 725c391..0000000
--- a/packages/mcp-server/src/skills-validator.ts
+++ /dev/null
@@ -1,476 +0,0 @@
-import path from 'node:path';
-import fs from 'node:fs';
-import matter from 'gray-matter';
-
-export const LIBRARY_RELATIVE_ROOT = 'plugin/skills';
-export const ROUTER_DIR = 'formio-api';
-export const REFERENCES_DIRNAME = 'references';
-export const SKILL_FILENAME = 'SKILL.md';
-
-export const REQUIRED_REFERENCE_GROUPS = [
- 'platform-auth',
- 'platform-projects',
- 'platform-teams',
- 'platform-staging',
- 'platform-tenants',
- 'project-auth',
- 'project-roles',
- 'project-forms',
- 'project-form-revisions',
- 'project-actions',
- 'runtime-auth',
- 'runtime-custom-users',
- 'runtime-access-control',
- 'runtime-reports',
- 'runtime-submissions',
- 'pdf-api',
- 'server-status',
-] as const;
-
-export type ReferenceGroup = (typeof REQUIRED_REFERENCE_GROUPS)[number];
-
-export const ALLOWED_SCOPES = ['platform', 'project', 'runtime', 'pdf'] as const;
-export type Scope = (typeof ALLOWED_SCOPES)[number];
-
-export const GROUP_SCOPE: Record = {
- 'platform-auth': 'platform',
- 'platform-projects': 'platform',
- 'platform-teams': 'platform',
- 'platform-staging': 'platform',
- 'platform-tenants': 'platform',
- 'server-status': 'platform',
- 'project-auth': 'project',
- 'project-roles': 'project',
- 'project-forms': 'project',
- 'project-form-revisions': 'project',
- 'project-actions': 'project',
- 'runtime-auth': 'runtime',
- 'runtime-custom-users': 'runtime',
- 'runtime-access-control': 'runtime',
- 'runtime-reports': 'runtime',
- 'runtime-submissions': 'runtime',
- 'pdf-api': 'pdf',
-};
-
-export const ROUTER_FRONTMATTER_KEYS = ['name', 'description'] as const;
-
-export const REQUIRED_REFERENCE_HEADINGS = [
- '## Overview',
- '## Root URL',
- '## Authentication',
- '## MCP Tool Preference',
- '## Endpoints',
-] as const;
-
-export const CANONICAL_AUTH_PARAGRAPH =
- "Every request to these endpoints MUST include an `x-jwt-token` header holding the user JWT issued by the MCP server's browser-based portal-login flow. The MCP server attaches this header automatically via `formioFetch`; external clients must obtain the JWT through the same portal-login flow. Do not use any other authentication mechanism with these endpoints.";
-
-export const MCP_PREFERENCE_HEADING = '## MCP Tool Preference';
-export const MCP_PREFERENCE_FALLBACK_SENTENCE =
- 'No MCP tool covers this operation — use the HTTP endpoint directly.';
-
-export const SERVER_STATUS_GROUP: ReferenceGroup = 'server-status';
-
-export const FORBIDDEN_TOKENS = ['x-token', 'FORMIO_API_KEY'] as const;
-export const FORBIDDEN_CASE_INSENSITIVE = ['api key'] as const;
-
-export interface ValidationIssue {
- file: string;
- rule: string;
- message: string;
-}
-
-function stripCodeFences(body: string): string {
- return body.replace(/```[\s\S]*?```/g, '').replace(/`[^`\n]*`/g, '');
-}
-
-function isRecord(value: unknown): value is Record {
- return typeof value === 'object' && value !== null && !Array.isArray(value);
-}
-
-export function parseSkillFile(source: string): { data: unknown; body: string } {
- const parsed = matter(source);
- return { data: parsed.data, body: parsed.content };
-}
-
-export function routerSkillPath(libraryDir: string): string {
- return path.join(libraryDir, ROUTER_DIR, SKILL_FILENAME);
-}
-
-export function referencePath(libraryDir: string, group: ReferenceGroup): string {
- return path.join(libraryDir, ROUTER_DIR, REFERENCES_DIRNAME, `${group}.md`);
-}
-
-export function validateRouterFrontmatter(file: string, data: unknown): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- if (!isRecord(data)) {
- issues.push({
- file,
- rule: 'frontmatter',
- message: `${file}: missing or malformed YAML frontmatter`,
- });
- return issues;
- }
- const requiredKeys = [...ROUTER_FRONTMATTER_KEYS].sort();
- const keys = Object.keys(data).sort();
- if (keys.length !== requiredKeys.length || keys.some((k, i) => k !== requiredKeys[i])) {
- issues.push({
- file,
- rule: 'frontmatter.keys',
- message: `${file}: router frontmatter keys must be exactly [${requiredKeys.join(', ')}], found [${keys.join(', ')}]`,
- });
- }
- for (const key of requiredKeys) {
- if (!(key in data) || typeof data[key] !== 'string' || data[key] === '') {
- issues.push({
- file,
- rule: `frontmatter.${key}`,
- message: `${file}: frontmatter.${key} must be a non-empty string`,
- });
- }
- }
- if (typeof data.name === 'string' && data.name !== ROUTER_DIR) {
- issues.push({
- file,
- rule: 'frontmatter.name',
- message: `${file}: router frontmatter.name must equal "${ROUTER_DIR}", got "${data.name}"`,
- });
- }
- return issues;
-}
-
-export function validateRouterDescriptionTriggers(file: string, data: unknown): ValidationIssue[] {
- if (!isRecord(data) || typeof data.description !== 'string') return [];
- const issues: ValidationIssue[] = [];
- if (!/use when/i.test(data.description)) {
- issues.push({
- file,
- rule: 'description.trigger_phrase',
- message: `${file}: router description must contain a trigger phrase ("Use when the user asks to …")`,
- });
- }
- if (!/not for:/i.test(data.description)) {
- issues.push({
- file,
- rule: 'description.negative_trigger',
- message: `${file}: router description must contain a "Not for: …" clause naming sibling orchestrator/plan skills`,
- });
- }
- return issues;
-}
-
-export function validateReferenceHasNoFrontmatter(file: string, data: unknown): ValidationIssue[] {
- if (isRecord(data) && Object.keys(data).length > 0) {
- return [
- {
- file,
- rule: 'reference.no_frontmatter',
- message: `${file}: reference files under formio-api/references/ must not have YAML frontmatter`,
- },
- ];
- }
- return [];
-}
-
-export function validateRequiredHeadings(file: string, body: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const lines = body.split('\n');
- const positions = REQUIRED_REFERENCE_HEADINGS.map((h) =>
- lines.findIndex((l) => l.trimStart() === h)
- );
- for (let i = 0; i < REQUIRED_REFERENCE_HEADINGS.length; i++) {
- if (positions[i] === -1) {
- issues.push({
- file,
- rule: 'headings.missing',
- message: `${file}: missing required heading "${REQUIRED_REFERENCE_HEADINGS[i]}"`,
- });
- }
- }
- for (let i = 1; i < REQUIRED_REFERENCE_HEADINGS.length; i++) {
- const prev = positions[i - 1];
- const cur = positions[i];
- if (prev !== -1 && cur !== -1 && cur < prev) {
- issues.push({
- file,
- rule: 'headings.order',
- message: `${file}: required headings are out of order — "${REQUIRED_REFERENCE_HEADINGS[i]}" appears before "${REQUIRED_REFERENCE_HEADINGS[i - 1]}"`,
- });
- }
- }
- return issues;
-}
-
-export function validateCanonicalAuthParagraph(
- file: string,
- body: string,
- group: ReferenceGroup
-): ValidationIssue[] {
- if (group === SERVER_STATUS_GROUP) return [];
- if (!body.includes(CANONICAL_AUTH_PARAGRAPH)) {
- return [
- {
- file,
- rule: 'auth.canonical_paragraph',
- message: `${file}: ## Authentication section must contain the canonical authentication paragraph verbatim`,
- },
- ];
- }
- return [];
-}
-
-export function validateForbiddenTokens(file: string, body: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- for (const token of FORBIDDEN_TOKENS) {
- if (body.includes(token)) {
- issues.push({
- file,
- rule: 'forbidden.legacy_auth',
- message: `${file}: forbidden legacy-auth reference "${token}" — references must use portal-login JWT only`,
- });
- }
- }
- const lower = body.toLowerCase();
- for (const phrase of FORBIDDEN_CASE_INSENSITIVE) {
- if (lower.includes(phrase)) {
- issues.push({
- file,
- rule: 'forbidden.legacy_auth',
- message: `${file}: forbidden legacy-auth phrase "${phrase}" (case-insensitive) — references must use portal-login JWT only`,
- });
- }
- }
- return issues;
-}
-
-export function validatePlaceholderSubstitution(
- file: string,
- body: string,
- scope: Scope
-): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const stripped = stripCodeFences(body);
- if (scope === 'project' || scope === 'runtime' || scope === 'pdf') {
- if (stripped.includes('{{baseUrl}}/{{projectName}}')) {
- issues.push({
- file,
- rule: 'placeholder.project',
- message: `${file}: Postman placeholder "{{baseUrl}}/{{projectName}}" must be resolved to \${FORMIO_PROJECT_URL}`,
- });
- }
- }
- if (scope === 'platform') {
- const bareBaseUrl = /\{\{baseUrl\}\}\/(?!\{\{projectName\}\})/;
- if (bareBaseUrl.test(stripped)) {
- issues.push({
- file,
- rule: 'placeholder.platform',
- message: `${file}: bare "{{baseUrl}}/" Postman placeholder must be resolved to \${FORMIO_BASE_URL}`,
- });
- }
- }
- return issues;
-}
-
-export function validatePdfProxyPath(file: string, body: string, scope: Scope): ValidationIssue[] {
- if (scope !== 'pdf') return [];
- const issues: ValidationIssue[] = [];
- const headingRegex = /^###\s+(GET|POST|PUT|PATCH|DELETE)\s+(\S+)/gm;
- let match: RegExpExecArray | null;
- while ((match = headingRegex.exec(body)) !== null) {
- const pathPart = match[2];
- if (!pathPart.startsWith('${FORMIO_PROJECT_URL}/pdf-proxy')) {
- issues.push({
- file,
- rule: 'pdf.proxy_path',
- message: `${file}: pdf-scope endpoint "${match[0].trim()}" must begin with \${FORMIO_PROJECT_URL}/pdf-proxy — "PDF server direct API" is out of scope`,
- });
- }
- }
- return issues;
-}
-
-export function validateTerminology(file: string, body: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const stripped = stripCodeFences(body);
- const projectMisuseRegex =
- /\b(baseUrl|base_url)\b[^.\n]*\b(project endpoint|project URL|project-scoped)\b/i;
- const platformMisuseRegex =
- /\b(projectUrl|project_url)\b[^.\n]*\b(platform endpoint|platform deployment|base URL|platform-scoped)\b/i;
- if (projectMisuseRegex.test(stripped)) {
- issues.push({
- file,
- rule: 'terminology.baseUrl_for_project',
- message: `${file}: "baseUrl"/"base_url" is reserved for the platform deployment endpoint. Use "projectUrl" or \${FORMIO_PROJECT_URL} to refer to the project endpoint.`,
- });
- }
- if (platformMisuseRegex.test(stripped)) {
- issues.push({
- file,
- rule: 'terminology.projectUrl_for_platform',
- message: `${file}: "projectUrl"/"project_url" is reserved for the project endpoint. Use "baseUrl" or \${FORMIO_BASE_URL} to refer to the platform deployment endpoint.`,
- });
- }
- return issues;
-}
-
-const RANDOM_ID_TITLE_REGEX = /"title"\s*:\s*"[^"\n]*[A-Za-z]\s\d{2,}"/g;
-const RANDOM_ID_SLUG_REGEX =
- /"(name|path|key|machineName)"\s*:\s*"[^"\n]*?[A-Za-z][A-Za-z0-9]*-\d{2,}(?=["/:])/g;
-
-export function validateNoRandomIdSuffixes(file: string, body: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const seen = new Set();
- const report = (match: RegExpExecArray) => {
- const snippet = match[0];
- if (seen.has(snippet)) return;
- seen.add(snippet);
- issues.push({
- file,
- rule: 'content.random_id_suffix',
- message: `${file}: example value contains a collision-avoidance integer suffix (${snippet}); strip the "-" or " " suffix for clean canonical examples`,
- });
- };
- let m: RegExpExecArray | null;
- RANDOM_ID_TITLE_REGEX.lastIndex = 0;
- while ((m = RANDOM_ID_TITLE_REGEX.exec(body)) !== null) report(m);
- RANDOM_ID_SLUG_REGEX.lastIndex = 0;
- while ((m = RANDOM_ID_SLUG_REGEX.exec(body)) !== null) report(m);
- return issues;
-}
-
-export function validateReferenceContent(
- filename: string,
- source: string,
- group: ReferenceGroup
-): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const { data, body } = parseSkillFile(source);
- issues.push(...validateReferenceHasNoFrontmatter(filename, data));
- issues.push(...validateRequiredHeadings(filename, body));
- issues.push(...validateCanonicalAuthParagraph(filename, body, group));
- issues.push(...validateForbiddenTokens(filename, body));
- const scope = GROUP_SCOPE[group];
- issues.push(...validatePlaceholderSubstitution(filename, body, scope));
- issues.push(...validatePdfProxyPath(filename, body, scope));
- issues.push(...validateTerminology(filename, body));
- issues.push(...validateNoRandomIdSuffixes(filename, body));
- return issues;
-}
-
-export function validateRouterSkillContent(filename: string, source: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const { data, body } = parseSkillFile(source);
- issues.push(...validateRouterFrontmatter(filename, data));
- issues.push(...validateRouterDescriptionTriggers(filename, data));
- issues.push(...validateForbiddenTokens(filename, body));
- issues.push(...validateTerminology(filename, body));
- return issues;
-}
-
-export function validateRequiredFiles(libraryDir: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- const routerFull = routerSkillPath(libraryDir);
- const routerRel = path.relative(libraryDir, routerFull);
- if (!fs.existsSync(routerFull)) {
- issues.push({
- file: routerRel,
- rule: 'library.required_file',
- message: `${routerFull}: required router skill file is missing`,
- });
- } else if (fs.statSync(routerFull).size === 0) {
- issues.push({
- file: routerRel,
- rule: 'library.required_file',
- message: `${routerFull}: router skill file is empty`,
- });
- }
-
- for (const group of REQUIRED_REFERENCE_GROUPS) {
- const full = referencePath(libraryDir, group);
- const rel = path.relative(libraryDir, full);
- if (!fs.existsSync(full)) {
- issues.push({
- file: rel,
- rule: 'library.required_file',
- message: `${full}: required reference file is missing`,
- });
- continue;
- }
- if (fs.statSync(full).size === 0) {
- issues.push({
- file: rel,
- rule: 'library.required_file',
- message: `${full}: reference file is empty`,
- });
- }
- }
- return issues;
-}
-
-export function validateRouterLinks(libraryDir: string): ValidationIssue[] {
- const routerFull = routerSkillPath(libraryDir);
- const routerRel = path.relative(libraryDir, routerFull);
- const issues: ValidationIssue[] = [];
- if (!fs.existsSync(routerFull)) return issues;
- const body = fs.readFileSync(routerFull, 'utf8');
-
- for (const group of REQUIRED_REFERENCE_GROUPS) {
- const linkPattern = new RegExp(`\\]\\(\\.?/?${REFERENCES_DIRNAME}/${group}\\.md\\)`);
- if (!linkPattern.test(body)) {
- issues.push({
- file: routerRel,
- rule: 'index.missing_link',
- message: `${routerFull}: router must link to "./${REFERENCES_DIRNAME}/${group}.md"`,
- });
- }
- }
-
- const linkRegex = new RegExp(`\\]\\((\\.?/?${REFERENCES_DIRNAME}/[\\w.-]+\\.md)\\)`, 'g');
- let linkMatch: RegExpExecArray | null;
- while ((linkMatch = linkRegex.exec(body)) !== null) {
- const target = linkMatch[1].replace(/^\.\//, '');
- const targetPath = path.join(libraryDir, ROUTER_DIR, target);
- if (!fs.existsSync(targetPath)) {
- issues.push({
- file: routerRel,
- rule: 'index.broken_link',
- message: `${routerFull}: link target "${target}" does not exist`,
- });
- }
- }
-
- const endpointHeadingRegex = /^###\s+(GET|POST|PUT|PATCH|DELETE)\s+\S+/m;
- if (endpointHeadingRegex.test(body)) {
- issues.push({
- file: routerRel,
- rule: 'index.no_endpoint_docs',
- message: `${routerFull}: router must not contain endpoint method/path headings — those belong in reference docs`,
- });
- }
-
- return issues;
-}
-
-export function validateLibrary(libraryDir: string): ValidationIssue[] {
- const issues: ValidationIssue[] = [];
- issues.push(...validateRequiredFiles(libraryDir));
- issues.push(...validateRouterLinks(libraryDir));
- if (!fs.existsSync(libraryDir)) return issues;
-
- const routerFull = routerSkillPath(libraryDir);
- if (fs.existsSync(routerFull)) {
- const source = fs.readFileSync(routerFull, 'utf8');
- const rel = path.relative(libraryDir, routerFull);
- issues.push(...validateRouterSkillContent(rel, source));
- }
-
- for (const group of REQUIRED_REFERENCE_GROUPS) {
- const full = referencePath(libraryDir, group);
- if (!fs.existsSync(full)) continue;
- const source = fs.readFileSync(full, 'utf8');
- const rel = path.relative(libraryDir, full);
- issues.push(...validateReferenceContent(rel, source, group));
- }
- return issues;
-}
diff --git a/packages/mcp-server/tsconfig.build.json b/packages/mcp-server/tsconfig.build.json
index 3185c48..51889f7 100644
--- a/packages/mcp-server/tsconfig.build.json
+++ b/packages/mcp-server/tsconfig.build.json
@@ -4,5 +4,5 @@
"sourceMap": false,
"declarationMap": false
},
- "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**", "src/skills-validator.ts"]
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
}
diff --git a/packages/skill-tests/README.md b/packages/skill-tests/README.md
new file mode 100644
index 0000000..960581d
--- /dev/null
+++ b/packages/skill-tests/README.md
@@ -0,0 +1,60 @@
+# @formio/skill-tests
+
+One-shot integration tests that verify the code examples shipped in the Form.io skill library at [`plugin/skills/`](../../plugin/skills/) actually work against the real Form.io SDK at runtime.
+
+Unlike a documentation linter, these tests don't parse the markdown — they hand-import the same modules each example imports (`@formio/js`, `@formio/js/utils`), call the same functions with the same arguments, and assert the same outputs. When the SDK changes its export shape or a reference doc drifts from the underlying source, the relevant test fails with a clear pointer to the bad example.
+
+## What's covered
+
+One Vitest file per reference doc under [`plugin/skills/formio-sdk/references/`](../../plugin/skills/formio-sdk/references/):
+
+| Reference doc | Test file | Style |
+| --- | --- | --- |
+| `setup.md`, `auth.md`, `forms.md`, `submissions.md`, `projects.md`, `roles.md`, `files.md`, `plugins.md`, `rendering.md` | [`src/formio-sdk/sdk-surface.test.ts`](src/formio-sdk/sdk-surface.test.ts) | Surface presence — every claimed static/instance method on `Formio` is asserted to be a callable function; URL config and the plugin lifecycle are exercised end-to-end. |
+| `utils-evaluator.md` | [`src/formio-sdk/utils-evaluator.test.ts`](src/formio-sdk/utils-evaluator.test.ts) | Execution — every example is run and its documented output is asserted. |
+| `utils-form-traversal.md` | [`src/formio-sdk/utils-form-traversal.test.ts`](src/formio-sdk/utils-form-traversal.test.ts) | Execution against a synthetic form definition. |
+| `utils-conditions.md` | [`src/formio-sdk/utils-conditions.test.ts`](src/formio-sdk/utils-conditions.test.ts) | Execution. |
+| `utils-logic.md` | [`src/formio-sdk/utils-logic.test.ts`](src/formio-sdk/utils-logic.test.ts) | Execution. |
+| `utils-jsonlogic.md` | [`src/formio-sdk/utils-jsonlogic.test.ts`](src/formio-sdk/utils-jsonlogic.test.ts) | Execution. |
+| `utils-mask-sanitize.md` | [`src/formio-sdk/utils-mask-sanitize.test.ts`](src/formio-sdk/utils-mask-sanitize.test.ts) | Execution. |
+| `utils-misc.md` | [`src/formio-sdk/utils-misc.test.ts`](src/formio-sdk/utils-misc.test.ts) | Execution. |
+
+The Vitest environment is `jsdom` because `@formio/js` pulls in DOM-dependent modules (dragula, DOMPurify, …) at import time.
+
+## Running
+
+```bash
+pnpm --filter @formio/skill-tests test
+```
+
+Or via the repo-wide Turbo pipeline:
+
+```bash
+pnpm test
+```
+
+## Interpreting failures
+
+These tests are deliberately one-shot — they are not maintained against a moving target. **A failing test is a signal that the skill reference doc and the SDK have diverged.** Two flavors:
+
+- **Missing surface** — the reference claims `Utils.X` or `Formio.Y` exists but it isn't exported under that name. Action: update the reference (the method may have been renamed, moved to a different namespace, or removed). Examples observed at the time of writing:
+ - `Utils.jsonLogic` (referenced in `utils-jsonlogic.md`) is not exposed on the `Utils` namespace.
+ - `Utils.registerEvaluator` is exported at the module level as `registerEvaluator`, not as a property of `Utils`.
+ - `Utils.dom`, `Utils.jwtDecode`, `Utils.date`, `Utils.I18n`, `Utils.unwind`, `Utils.override`, `Utils.checkLegacyConditional` are referenced in the skill but not on `Utils`.
+ - `Formio.clearTokens` is referenced but not exported; the runtime exposes `Formio.tokens` and `Formio.clearCache` instead.
+ - `Formio.Providers.storage` is referenced but the real path is `Formio.Providers.providers.storage` (plus `addProvider`).
+ - `new Formio(projectUrl).projectRoles` is referenced but the instance method does not exist; only the static `Formio.projectRoles(formio?)` does.
+
+- **Documented behavior differs** — the reference shows an example output that the SDK does not produce. Examples observed:
+ - `Utils.matchInputMask('acm-001', getInputMask('AAA-999'))` returns `true` (the SDK's `A` token is case-insensitive), but the example asserts `false`.
+ - `Utils.getComponentValue(form, data, 'data.address.line1')` returns `undefined`; the SDK expects the path **without** the leading `data.` prefix (`'address.line1'`).
+ - `Utils.eachComponentData` callback's `contextualData` parameter is the full data tree, not the row of data at the current path. The example's `contextualData[component.key]` returns `undefined` for nested components — the `row` argument is what the example actually needs.
+ - `Utils.Evaluator.interpolateString(..., { noeval: true })` returns the user-supplied string verbatim (does not HTML-escape), contrary to the example's comment.
+ - `Utils.eachComponent` skips layout components (panel) by default, so `return true` in the callback never fires for them — the "stop descent into hidden containers" example needs `includeAll` to actually visit and short-circuit the panel.
+ - `Utils.checkTrigger(..., { type: 'javascript', javascript: 'result = data.qty > 10' })` throws — the JS-trigger evaluator does not expose `data` directly as documented.
+
+## Adding tests for a new skill
+
+1. Add a `src//` directory and one test file per reference doc.
+2. Mirror each example block: import the same modules, call the same APIs with the same arguments, and assert the documented output.
+3. Prefer execution tests over surface-presence tests where possible — execution catches behavior drift, surface presence catches only export drift.
diff --git a/packages/skill-tests/package.json b/packages/skill-tests/package.json
new file mode 100644
index 0000000..f313ca0
--- /dev/null
+++ b/packages/skill-tests/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@formio/skill-tests",
+ "version": "0.0.0",
+ "private": true,
+ "description": "Automated structural tests for code examples shipped in the Form.io skills library.",
+ "type": "module",
+ "license": "MIT",
+ "scripts": {
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "lint": "tsc --noEmit",
+ "typecheck": "tsc --noEmit",
+ "clean": "rm -rf coverage"
+ },
+ "devDependencies": {
+ "@formio/core": "^2.6.6",
+ "@formio/js": "^5.3.6",
+ "@types/node": "^22.19.17",
+ "jsdom": "^25.0.1",
+ "typescript": "^5.8.2",
+ "vitest": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+}
diff --git a/packages/skill-tests/src/formio-sdk/sdk-surface.test.ts b/packages/skill-tests/src/formio-sdk/sdk-surface.test.ts
new file mode 100644
index 0000000..1a58472
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/sdk-surface.test.ts
@@ -0,0 +1,302 @@
+// @ts-nocheck — see utils-evaluator.test.ts for rationale.
+// Surface-presence tests for the SDK references that exercise network /
+// authentication endpoints (`setup.md`, `auth.md`, `forms.md`,
+// `submissions.md`, `projects.md`, `roles.md`, `files.md`, `plugins.md`,
+// `rendering.md`). These tests do not hit a real Form.io server — they
+// verify that every method the references claim is actually exposed by
+// `@formio/js`, with the right callable shape, so the reference doc cannot
+// silently drift from the runtime SDK.
+
+import { afterEach, beforeAll, describe, expect, it } from 'vitest';
+import { Formio } from '@formio/js';
+
+type FormioStatic = typeof Formio & Record;
+type FormioInstance = InstanceType & Record;
+
+const HOSTED_BASE = 'https://forms.mysite.com';
+const HOSTED_PROJECT = 'https://forms.mysite.com/myproject';
+const SAAS_BASE = 'https://api.form.io';
+const SAAS_PROJECT = 'https://myproject.form.io';
+
+function staticMethod(name: string): unknown {
+ return (Formio as unknown as Record)[name];
+}
+
+function instanceMethod(formio: FormioInstance, name: string): unknown {
+ return formio[name];
+}
+
+beforeAll(() => {
+ Formio.setBaseUrl(HOSTED_BASE);
+ Formio.setProjectUrl(HOSTED_PROJECT);
+});
+
+afterEach(() => {
+ Formio.setBaseUrl(HOSTED_BASE);
+ Formio.setProjectUrl(HOSTED_PROJECT);
+});
+
+describe('setup.md surface', () => {
+ it('Formio.setBaseUrl / getBaseUrl round-trip', () => {
+ Formio.setBaseUrl(HOSTED_BASE);
+ expect(Formio.getBaseUrl()).toBe(HOSTED_BASE);
+ Formio.setBaseUrl(SAAS_BASE);
+ expect(Formio.getBaseUrl()).toBe(SAAS_BASE);
+ });
+
+ it('Formio.setProjectUrl / getProjectUrl round-trip', () => {
+ Formio.setProjectUrl(HOSTED_PROJECT);
+ expect(Formio.getProjectUrl()).toBe(HOSTED_PROJECT);
+ Formio.setProjectUrl(SAAS_PROJECT);
+ expect(Formio.getProjectUrl()).toBe(SAAS_PROJECT);
+ });
+
+ it('Formio.setAuthUrl is exposed', () => {
+ expect(typeof staticMethod('setAuthUrl')).toBe('function');
+ });
+
+ it('Formio.setPathType is exposed', () => {
+ expect(typeof staticMethod('setPathType')).toBe('function');
+ });
+
+ it('Formio.setToken / getToken are exposed and setToken(null) clears the cached JWT', async () => {
+ expect(typeof staticMethod('setToken')).toBe('function');
+ expect(typeof staticMethod('getToken')).toBe('function');
+ // Install a fake token then clear it via setToken(null) — there is no
+ // dedicated clearTokens helper. setToken always issues a `/current`
+ // fetch to populate the cached user; that fetch fails offline, so
+ // swallow the rejection — the token-cache mutation happens regardless.
+ const token =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMiLCJleHAiOjk5OTk5OTk5OTl9.sig';
+ await Formio.setToken(token).catch(() => undefined);
+ expect(typeof Formio.getToken()).toBe('string');
+ await Formio.setToken(null).catch(() => undefined);
+ expect(Formio.getToken()).toBeFalsy();
+ });
+
+ it('Formio.setUser / getUser are exposed', () => {
+ expect(typeof staticMethod('setUser')).toBe('function');
+ expect(typeof staticMethod('getUser')).toBe('function');
+ });
+
+ it('Formio.requireLibrary / libraryReady are exposed', () => {
+ expect(typeof staticMethod('requireLibrary')).toBe('function');
+ expect(typeof staticMethod('libraryReady')).toBe('function');
+ });
+
+ it('Formio.cdn exposes baseUrl/setBaseUrl/libs', () => {
+ const cdn = (Formio as unknown as { cdn?: Record }).cdn;
+ expect(cdn).toBeTruthy();
+ expect(typeof cdn?.baseUrl).toBe('string');
+ expect(typeof cdn?.setBaseUrl).toBe('function');
+ expect(cdn?.libs).toBeTruthy();
+ });
+
+ it('Formio.addLibrary / addLoader are exposed', () => {
+ expect(typeof staticMethod('addLibrary')).toBe('function');
+ expect(typeof staticMethod('addLoader')).toBe('function');
+ });
+});
+
+describe('auth.md surface', () => {
+ const authStatics = [
+ 'currentUser',
+ 'logout',
+ 'setToken',
+ 'getToken',
+ 'ssoInit',
+ 'samlInit',
+ 'oktaInit',
+ 'oAuthCurrentUser',
+ 'oauthLogoutURI',
+ 'pageQuery',
+ ];
+ for (const name of authStatics) {
+ it(`Formio.${name} is exposed`, () => {
+ expect(typeof staticMethod(name)).toBe('function');
+ });
+ }
+});
+
+describe('forms.md surface', () => {
+ function formInstance(): FormioInstance {
+ return new Formio(`${HOSTED_PROJECT}/intake`) as FormioInstance;
+ }
+
+ it('Formio instance resolves form URL fields', () => {
+ const formio = formInstance();
+ expect(formio.projectUrl).toBe(HOSTED_PROJECT);
+ expect(formio.formUrl).toBe(`${HOSTED_PROJECT}/intake`);
+ expect(formio.formsUrl).toBe(`${HOSTED_PROJECT}/form`);
+ });
+
+ const formMethods = ['loadForm', 'saveForm', 'deleteForm', 'loadForms', 'getFormId'];
+ for (const name of formMethods) {
+ it(`new Formio(formUrl).${name} is a function`, () => {
+ expect(typeof instanceMethod(formInstance(), name)).toBe('function');
+ });
+ }
+
+ it('Formio.clearCache is exposed', () => {
+ expect(typeof staticMethod('clearCache')).toBe('function');
+ });
+});
+
+describe('submissions.md surface', () => {
+ function subInstance(): FormioInstance {
+ return new Formio(
+ `${HOSTED_PROJECT}/intake/submission/000000000000000000000010`
+ ) as FormioInstance;
+ }
+
+ const subMethods = [
+ 'loadSubmission',
+ 'saveSubmission',
+ 'deleteSubmission',
+ 'loadSubmissions',
+ 'availableActions',
+ 'actionInfo',
+ 'userPermissions',
+ 'canSubmit',
+ 'getDownloadUrl',
+ 'getTempToken',
+ ];
+ for (const name of subMethods) {
+ it(`new Formio(submissionUrl).${name} is a function`, () => {
+ expect(typeof instanceMethod(subInstance(), name)).toBe('function');
+ });
+ }
+});
+
+describe('projects.md surface', () => {
+ function projInstance(): FormioInstance {
+ return new Formio(HOSTED_PROJECT) as FormioInstance;
+ }
+
+ const projMethods = ['loadProject', 'saveProject', 'deleteProject', 'accessInfo', 'getProjectId'];
+ for (const name of projMethods) {
+ it(`new Formio(projectUrl).${name} is a function`, () => {
+ expect(typeof instanceMethod(projInstance(), name)).toBe('function');
+ });
+ }
+
+ it('Formio.accessInfo / Formio.projectRoles static helpers are exposed', () => {
+ expect(typeof staticMethod('accessInfo')).toBe('function');
+ expect(typeof staticMethod('projectRoles')).toBe('function');
+ });
+});
+
+describe('roles.md surface', () => {
+ function roleInstance(): FormioInstance {
+ return new Formio(`${HOSTED_PROJECT}/role/000000000000000000000020`) as FormioInstance;
+ }
+
+ const roleMethods = ['loadRole', 'saveRole', 'deleteRole', 'loadRoles'];
+ for (const name of roleMethods) {
+ it(`new Formio(roleUrl).${name} is a function`, () => {
+ expect(typeof instanceMethod(roleInstance(), name)).toBe('function');
+ });
+ }
+});
+
+describe('files.md surface', () => {
+ function fileInstance(): FormioInstance {
+ return new Formio(`${HOSTED_PROJECT}/intake`) as FormioInstance;
+ }
+
+ const fileMethods = ['uploadFile', 'downloadFile', 'deleteFile'];
+ for (const name of fileMethods) {
+ it(`new Formio(formUrl).${name} is a function`, () => {
+ expect(typeof instanceMethod(fileInstance(), name)).toBe('function');
+ });
+ }
+
+ it('Formio.Providers.providers.storage registry is exposed', () => {
+ const Providers = (
+ Formio as unknown as {
+ Providers?: {
+ providers?: { storage?: Record };
+ addProvider?: unknown;
+ };
+ }
+ ).Providers;
+ expect(Providers).toBeTruthy();
+ expect(Providers?.providers?.storage).toBeTruthy();
+ expect(typeof Providers?.addProvider).toBe('function');
+ });
+});
+
+describe('plugins.md surface', () => {
+ const pluginStatics = [
+ 'registerPlugin',
+ 'deregisterPlugin',
+ 'getPlugin',
+ 'pluginAlter',
+ 'pluginGet',
+ 'pluginWait',
+ ];
+ for (const name of pluginStatics) {
+ it(`Formio.${name} is exposed`, () => {
+ expect(typeof staticMethod(name)).toBe('function');
+ });
+ }
+
+ it('register / lookup / deregister a plugin', () => {
+ const plugin = {
+ __name: 'logger',
+ priority: 1,
+ preRequest() {
+ // no-op
+ },
+ };
+ Formio.registerPlugin(plugin, 'logger');
+ expect(Formio.getPlugin('logger')).toBe(plugin);
+ expect(Formio.deregisterPlugin('logger')).toBe(true);
+ expect(Formio.getPlugin('logger')).toBeFalsy();
+ });
+
+ it('pluginAlter folds a value through registered plugins', () => {
+ const plugin = {
+ __name: 'rewriter',
+ priority: 1,
+ rewriteUrl(value: string) {
+ return `${value}?via=plugin`;
+ },
+ };
+ Formio.registerPlugin(plugin, 'rewriter');
+ try {
+ const result = (Formio as unknown as FormioStatic).pluginAlter as (
+ hook: string,
+ value: unknown,
+ ...args: unknown[]
+ ) => unknown;
+ expect(result('rewriteUrl', 'https://example/x', { formId: 'x' })).toBe(
+ 'https://example/x?via=plugin'
+ );
+ } finally {
+ Formio.deregisterPlugin('rewriter');
+ }
+ });
+});
+
+describe('rendering.md surface', () => {
+ const renderStatics = ['createForm', 'builder', 'use', 'formioReady'];
+ for (const name of renderStatics) {
+ it(`Formio.${name} is exposed`, () => {
+ const value = staticMethod(name);
+ // `formioReady` is a Promise, others are callable.
+ if (name === 'formioReady') {
+ expect(value).toBeTruthy();
+ expect(typeof (value as { then?: unknown })?.then).toBe('function');
+ } else {
+ expect(typeof value).toBe('function');
+ }
+ });
+ }
+
+ it('Formio.Templates / Formio.icons hooks are exposed', () => {
+ expect((Formio as unknown as { Templates?: unknown }).Templates).toBeTruthy();
+ // `icons` may be undefined until a template framework is loaded — only
+ // assert the Templates hook the reference's API table calls out.
+ });
+});
diff --git a/packages/skill-tests/src/formio-sdk/utils-conditions.test.ts b/packages/skill-tests/src/formio-sdk/utils-conditions.test.ts
new file mode 100644
index 0000000..dcf1573
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/utils-conditions.test.ts
@@ -0,0 +1,85 @@
+// @ts-nocheck — see utils-evaluator.test.ts for rationale.
+// Mirrors every example in
+// `plugin/skills/formio-sdk/references/utils-conditions.md`.
+
+import { describe, expect, it } from 'vitest';
+import { Utils } from '@formio/js/utils';
+
+describe('utils-conditions.md examples', () => {
+ it('Evaluate a simple conditional', () => {
+ const conditional = {
+ conjunction: 'all',
+ conditions: [
+ { component: 'subscribe', operator: 'isEqual', value: true },
+ { component: 'region', operator: 'isEqual', value: 'EU' },
+ ],
+ show: true,
+ };
+
+ const data = { subscribe: true, region: 'EU' };
+ const visible = Utils.checkSimpleConditional({}, conditional, data, data, null);
+ expect(visible).toBe(true);
+ });
+
+ it('Evaluate a JSONLogic conditional', () => {
+ const json = { '>': [{ var: 'data.age' }, 17] };
+ const data = { age: 21 };
+ const eligible = Utils.checkJsonConditional({}, json, data, data, null, false);
+ expect(eligible).toBe(true);
+ });
+
+ it('Evaluate a custom JavaScript conditional', () => {
+ const data = { kind: 'premium', seats: 3 };
+ const visible = Utils.checkCustomConditional(
+ {},
+ 'show = data.kind === "premium" && data.seats > 0;',
+ data,
+ data,
+ null,
+ 'show',
+ false
+ );
+ expect(visible).toBe(true);
+ });
+
+ it('Evaluate via the high-level helper', () => {
+ const form = {
+ components: [
+ {
+ type: 'textfield',
+ key: 'discount',
+ input: true,
+ conditional: {
+ json: { '>': [{ var: 'data.total' }, 100] },
+ },
+ },
+ ],
+ };
+ const submission = { data: { total: 250 } };
+
+ const results: Record = {};
+ Utils.eachComponent(form.components, (component) => {
+ if (!component.conditional) return;
+ results[component.key] = Utils.checkCondition(
+ component,
+ submission.data,
+ submission.data,
+ form,
+ null
+ );
+ });
+ expect(results.discount).toBe(true);
+ });
+
+ it('Handle a legacy when / eq / show conditional via checkSimpleConditional', () => {
+ const data = { country: 'US' };
+ const visible = Utils.checkSimpleConditional(
+ {},
+ { when: 'country', eq: 'US', show: 'true' },
+ data,
+ data,
+ null
+ );
+ expect(visible).toBe(true);
+ });
+});
diff --git a/packages/skill-tests/src/formio-sdk/utils-evaluator.test.ts b/packages/skill-tests/src/formio-sdk/utils-evaluator.test.ts
new file mode 100644
index 0000000..74468b8
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/utils-evaluator.test.ts
@@ -0,0 +1,84 @@
+// @ts-nocheck — these tests deliberately exercise the runtime surface of
+// `@formio/js/utils` exactly as documented in the skill references, even
+// where the published `.d.ts` declarations are stricter or out of sync with
+// the runtime. The goal is to detect doc-vs-runtime drift; TypeScript types
+// are the wrong oracle for that.
+//
+// Mirrors every example in
+// `plugin/skills/formio-sdk/references/utils-evaluator.md` and asserts that
+// the documented behavior matches what `@formio/js/utils` actually exposes at
+// runtime. Tests are deliberately one-shot — if `@formio/js` changes its
+// Utils surface, these tests reveal the drift so the reference doc can be
+// updated.
+
+import { describe, expect, it } from 'vitest';
+import { Utils, registerEvaluator, DefaultEvaluator } from '@formio/js/utils';
+
+describe('utils-evaluator.md examples', () => {
+ it('Interpolate a template string', () => {
+ const greeting = Utils.Evaluator.interpolateString('Hello {{ data.firstName }}!', {
+ data: { firstName: 'Alice' },
+ });
+ expect(greeting).toBe('Hello Alice!');
+ });
+
+ it('Evaluate a custom validation expression', () => {
+ const valid = Utils.Evaluator.evaluate(
+ 'valid = data.age >= 18;',
+ { data: { age: 21 } },
+ 'valid'
+ );
+ expect(valid).toBe(true);
+ });
+
+ it('Evaluate a JSONLogic expression', () => {
+ const ok = Utils.Evaluator.evaluate({ '>=': [{ var: 'data.age' }, 18] }, { data: { age: 21 } });
+ expect(ok).toBe(true);
+ });
+
+ it('Compile and reuse a function', () => {
+ const fn = Utils.Evaluator.evaluator('return data.first + " " + data.last;', 'data');
+ expect(typeof fn).toBe('function');
+ expect(fn({ first: 'Ada', last: 'Lovelace' })).toBe('Ada Lovelace');
+ });
+
+ it('interpolateString does NOT HTML-escape; pair with sanitize for safe HTML output', () => {
+ const raw = Utils.Evaluator.interpolateString('Hello {{ data.firstName }}
', {
+ data: { firstName: '
' },
+ });
+ // interpolateString substitutes verbatim — the raw output is dangerous.
+ expect(raw).toContain('
Hello');
+ });
+
+ it('registerEvaluator + DefaultEvaluator are top-level exports usable to install a sandboxed evaluator', () => {
+ // Doc notes that under ESM-import semantics the swap does not retroactively
+ // rebind references that consumer code already holds (including
+ // `Utils.Evaluator`) — only SDK-internal evaluations see the override.
+ // The exposed surface is what we verify here.
+ expect(typeof registerEvaluator).toBe('function');
+ expect(typeof DefaultEvaluator).toBe('function');
+
+ class Sandboxed extends DefaultEvaluator {}
+ expect(() => registerEvaluator(new Sandboxed({ noeval: true }))).not.toThrow();
+ // Restore default so other tests are not affected.
+ registerEvaluator(new DefaultEvaluator());
+ });
+
+ it('module-level convenience: Utils.interpolate delegates to Evaluator.interpolate', () => {
+ expect(typeof Utils.interpolate).toBe('function');
+ const out = Utils.interpolate('{{ data.x }}', { data: { x: 7 } });
+ expect(out).toBe('7');
+ });
+
+ it('module-level convenience: Utils.evaluate delegates to Evaluator.evaluate', () => {
+ expect(typeof Utils.evaluate).toBe('function');
+ const result = Utils.evaluate('value = data.a + data.b;', { data: { a: 2, b: 3 } }, 'value');
+ expect(result).toBe(5);
+ });
+});
diff --git a/packages/skill-tests/src/formio-sdk/utils-form-traversal.test.ts b/packages/skill-tests/src/formio-sdk/utils-form-traversal.test.ts
new file mode 100644
index 0000000..4e31c82
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/utils-form-traversal.test.ts
@@ -0,0 +1,136 @@
+// @ts-nocheck — see utils-evaluator.test.ts for rationale.
+// Mirrors every example in
+// `plugin/skills/formio-sdk/references/utils-form-traversal.md` against a
+// synthetic Form.io form definition. Pure logic — no network, no DOM.
+
+import { describe, expect, it } from 'vitest';
+import { Utils } from '@formio/js/utils';
+
+const form = {
+ display: 'form',
+ components: [
+ { type: 'textfield', key: 'firstName', label: 'First Name', input: true },
+ {
+ type: 'email',
+ key: 'email',
+ label: 'Email',
+ input: true,
+ validate: { required: true },
+ },
+ {
+ type: 'container',
+ key: 'address',
+ input: true,
+ components: [
+ {
+ type: 'textfield',
+ key: 'line1',
+ label: 'Line 1',
+ input: true,
+ validate: { required: true },
+ },
+ { type: 'textfield', key: 'city', label: 'City', input: true },
+ ],
+ },
+ {
+ type: 'panel',
+ key: 'hiddenPanel',
+ hidden: true,
+ components: [{ type: 'textfield', key: 'secret', label: 'Secret', input: true }],
+ },
+ { type: 'select', key: 'country', label: 'Country', input: true, data: {} },
+ ],
+};
+
+const submission = {
+ data: {
+ firstName: 'Ada',
+ email: 'ada@example.com',
+ address: { line1: '1 Park Ave', city: 'NYC' },
+ secret: 'should-not-see',
+ country: 'US',
+ },
+};
+
+describe('utils-form-traversal.md examples', () => {
+ it('Walk every component', () => {
+ const seen: string[] = [];
+ Utils.eachComponent(form.components, (component, path) => {
+ seen.push(`${path}:${component.type}`);
+ });
+ expect(seen).toEqual(
+ expect.arrayContaining([
+ 'firstName:textfield',
+ 'email:email',
+ 'address:container',
+ 'address.line1:textfield',
+ 'address.city:textfield',
+ 'country:select',
+ ])
+ );
+ });
+
+ it('Walk components alongside their data', () => {
+ const collected: Record = {};
+ Utils.eachComponentData(form.components, submission.data, (component, _data, row) => {
+ if (component.input) {
+ collected[component.key] = (row as Record)[component.key];
+ }
+ });
+ expect(collected.firstName).toBe('Ada');
+ expect(collected.email).toBe('ada@example.com');
+ expect(collected.line1).toBe('1 Park Ave');
+ });
+
+ it('Find a component by key', () => {
+ const email = Utils.getComponent(form.components, 'email');
+ expect(email).toBeTruthy();
+ expect(email?.type).toBe('email');
+ if (email) email.label = 'Work Email';
+ expect(Utils.getComponent(form.components, 'email')?.label).toBe('Work Email');
+ });
+
+ it('Query components by attribute', () => {
+ const required = Utils.searchComponents(form.components, {
+ 'validate.required': true,
+ });
+ const keys = required.map((c) => c.key).sort();
+ expect(keys).toEqual(['email', 'line1']);
+ });
+
+ it('Flatten to a path map', () => {
+ const map = Utils.flattenComponents(form.components);
+ const keys = Object.keys(map);
+ expect(keys.length).toBeGreaterThan(0);
+ expect(keys).toEqual(expect.arrayContaining(['firstName', 'email']));
+ });
+
+ it('Read a value at a deep path', () => {
+ const value = Utils.getComponentValue(form, submission.data, 'address.line1');
+ expect(value).toBe('1 Park Ave');
+ });
+
+ it('Async traversal with side effects', async () => {
+ const visited: string[] = [];
+ await Utils.eachComponentAsync(form.components, async (component) => {
+ visited.push(component.key);
+ });
+ expect(visited).toEqual(expect.arrayContaining(['firstName', 'email', 'country']));
+ });
+
+ it('Stop descent into hidden containers (includeAll + return true)', () => {
+ const reached: string[] = [];
+ Utils.eachComponent(
+ form.components,
+ (component) => {
+ reached.push(component.key);
+ if (component.hidden) return true;
+ return undefined;
+ },
+ true
+ );
+ // The hidden panel itself is visited, but its children are not.
+ expect(reached).toContain('hiddenPanel');
+ expect(reached).not.toContain('secret');
+ });
+});
diff --git a/packages/skill-tests/src/formio-sdk/utils-jsonlogic.test.ts b/packages/skill-tests/src/formio-sdk/utils-jsonlogic.test.ts
new file mode 100644
index 0000000..6e858ef
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/utils-jsonlogic.test.ts
@@ -0,0 +1,80 @@
+// @ts-nocheck — see utils-evaluator.test.ts for rationale.
+// Mirrors every example in
+// `plugin/skills/formio-sdk/references/utils-jsonlogic.md`.
+
+import { describe, expect, it } from 'vitest';
+import { jsonLogic } from '@formio/core';
+
+describe('utils-jsonlogic.md examples', () => {
+ it('jsonLogic engine is exposed by @formio/core', () => {
+ expect(jsonLogic).toBeTruthy();
+ expect(typeof jsonLogic.apply).toBe('function');
+ expect(typeof jsonLogic.add_operation).toBe('function');
+ expect(typeof jsonLogic.rm_operation).toBe('function');
+ expect(typeof jsonLogic.uses_data).toBe('function');
+ expect(typeof jsonLogic.truthy).toBe('function');
+ });
+
+ it('Evaluate a simple rule', () => {
+ const isAdult = jsonLogic.apply({ '>=': [{ var: 'data.age' }, 18] }, { data: { age: 21 } });
+ expect(isAdult).toBe(true);
+ });
+
+ it('Compute a derived value', () => {
+ const total = jsonLogic.apply(
+ { '*': [{ var: 'data.qty' }, { var: 'data.unitPrice' }] },
+ { data: { qty: 3, unitPrice: 19.99 } }
+ );
+ expect(total).toBeCloseTo(59.97, 2);
+ });
+
+ it('Combine conditions', () => {
+ const eligible = jsonLogic.apply(
+ {
+ and: [
+ { '==': [{ var: 'data.country' }, 'US'] },
+ { '>=': [{ var: 'data.age' }, 21] },
+ { '==': [{ var: 'data.consent' }, true] },
+ ],
+ },
+ { data: { country: 'US', age: 25, consent: true } }
+ );
+ expect(eligible).toBe(true);
+ });
+
+ it('Form.io date helpers are registered (relativeMaxDate)', () => {
+ const within30 = jsonLogic.apply(
+ {
+ '<=': [{ var: 'data.appointment' }, { relativeMaxDate: [30] }],
+ },
+ { data: { appointment: '2026-06-01T00:00:00.000Z' } }
+ );
+ expect(typeof within30).toBe('boolean');
+ });
+
+ it('Register a custom operator', () => {
+ jsonLogic.add_operation(
+ 'startsWith',
+ (str: unknown, prefix: unknown) =>
+ typeof str === 'string' && typeof prefix === 'string' && str.startsWith(prefix)
+ );
+
+ try {
+ const ok = jsonLogic.apply(
+ { startsWith: [{ var: 'data.sku' }, 'ACME-'] },
+ { data: { sku: 'ACME-1234' } }
+ );
+ expect(ok).toBe(true);
+ } finally {
+ jsonLogic.rm_operation('startsWith');
+ }
+ });
+
+ it('Inspect dependencies before evaluating', () => {
+ const rule = {
+ and: [{ '==': [{ var: 'data.country' }, 'US'] }, { '>=': [{ var: 'data.age' }, 21] }],
+ };
+ const deps = jsonLogic.uses_data(rule).sort();
+ expect(deps).toEqual(['data.age', 'data.country']);
+ });
+});
diff --git a/packages/skill-tests/src/formio-sdk/utils-logic.test.ts b/packages/skill-tests/src/formio-sdk/utils-logic.test.ts
new file mode 100644
index 0000000..6f1f44e
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/utils-logic.test.ts
@@ -0,0 +1,130 @@
+// @ts-nocheck — see utils-evaluator.test.ts for rationale.
+// Mirrors every example in
+// `plugin/skills/formio-sdk/references/utils-logic.md`.
+
+import { describe, expect, it } from 'vitest';
+import { Utils } from '@formio/js/utils';
+import { logicProcessSync, logicProcessInfo } from '@formio/core/process';
+
+describe('utils-logic.md examples', () => {
+ it('Check a single JavaScript trigger', () => {
+ const component = {
+ key: 'qty',
+ type: 'number',
+ input: true,
+ logic: [],
+ };
+
+ const data = { qty: 50 };
+ const fired = Utils.checkTrigger(
+ component,
+ { type: 'javascript', javascript: 'result = data.qty > 10;' },
+ data,
+ data,
+ { components: [component] },
+ null
+ );
+ expect(fired).toBe(true);
+ });
+
+ it('Apply a "set hidden when approved" rule via logicProcessSync', () => {
+ const component = {
+ key: 'managerApproval',
+ type: 'textfield',
+ input: true,
+ hidden: false,
+ logic: [
+ {
+ name: 'hide once approved',
+ trigger: {
+ type: 'javascript',
+ javascript: 'result = data.status === "approved";',
+ },
+ actions: [
+ {
+ type: 'property',
+ property: {
+ type: 'boolean',
+ label: 'Hidden',
+ value: 'hidden',
+ component: 'hidden',
+ },
+ state: true,
+ },
+ ],
+ },
+ ],
+ };
+
+ const data = { status: 'approved' };
+ logicProcessSync({
+ component,
+ data,
+ row: data,
+ form: { components: [component] },
+ path: 'managerApproval',
+ scope: {},
+ });
+
+ expect(component.hidden).toBe(true);
+ });
+
+ it('Compute a derived value via a value action', () => {
+ const component = {
+ key: 'total',
+ type: 'number',
+ input: true,
+ logic: [
+ {
+ name: 'sum',
+ trigger: { type: 'javascript', javascript: 'result = true;' },
+ actions: [{ type: 'value', value: 'value = data.qty * data.unitPrice;' }],
+ },
+ ],
+ };
+
+ const data: Record = { qty: 3, unitPrice: 19.99 };
+ logicProcessSync({
+ component,
+ data,
+ row: data,
+ form: { components: [component] },
+ path: 'total',
+ scope: {},
+ });
+
+ expect(data.total).toBeCloseTo(59.97, 2);
+ });
+
+ it('Run logic for every component in a form (hasLogic guard via logicProcessInfo.shouldProcess)', () => {
+ const totalComponent = {
+ key: 'total',
+ type: 'number',
+ input: true,
+ logic: [
+ {
+ name: 'sum',
+ trigger: { type: 'javascript', javascript: 'result = true;' },
+ actions: [{ type: 'value', value: 'value = data.qty + 1;' }],
+ },
+ ],
+ };
+ const noLogicComponent = { key: 'qty', type: 'number', input: true };
+ const form = { components: [noLogicComponent, totalComponent] };
+ const data: Record = { qty: 5 };
+
+ for (const component of form.components) {
+ if (!logicProcessInfo.shouldProcess({ component })) continue;
+ logicProcessSync({
+ component,
+ data,
+ row: data,
+ form,
+ path: component.key,
+ scope: {},
+ });
+ }
+
+ expect(data.total).toBe(6);
+ });
+});
diff --git a/packages/skill-tests/src/formio-sdk/utils-mask-sanitize.test.ts b/packages/skill-tests/src/formio-sdk/utils-mask-sanitize.test.ts
new file mode 100644
index 0000000..7d63c2b
--- /dev/null
+++ b/packages/skill-tests/src/formio-sdk/utils-mask-sanitize.test.ts
@@ -0,0 +1,74 @@
+// @ts-nocheck — see utils-evaluator.test.ts for rationale.
+// Mirrors every example in
+// `plugin/skills/formio-sdk/references/utils-mask-sanitize.md`.
+
+import { describe, expect, it } from 'vitest';
+import { Utils as TypedUtils } from '@formio/js/utils';
+import { dom } from '@formio/core';
+
+// The published TS types for `getInputMask` / `matchInputMask` are stricter
+// than the runtime — the references and the actual SDK accept the looser
+// signatures used below. Cast through `any` so the tests exercise the
+// documented call shape exactly.
+const Utils = TypedUtils as unknown as {
+ getInputMask: (mask: string, placeholderChar?: string) => unknown[];
+ matchInputMask: (value: string, mask: unknown[]) => boolean;
+ sanitize: (html: string, options: Record) => unknown;
+};
+
+describe('utils-mask-sanitize.md examples', () => {
+ it('Build a phone-number mask', () => {
+ const mask = Utils.getInputMask('(999) 999-9999');
+ expect(Array.isArray(mask)).toBe(true);
+ expect(Utils.matchInputMask('(415) 555-1212', mask)).toBe(true);
+ expect(Utils.matchInputMask('415-555-1212', mask)).toBe(false);
+ });
+
+ it('Mask a SKU (A token is case-insensitive)', () => {
+ const mask = Utils.getInputMask('AAA-999');
+ expect(Utils.matchInputMask('ACM-001', mask)).toBe(true);
+ expect(Utils.matchInputMask('acm-001', mask)).toBe(true);
+ expect(Utils.matchInputMask('A1M-001', mask)).toBe(false);
+ });
+
+ it('Sanitize HTML before rendering — strips world
', {});
+ const html = safe.toString();
+ expect(html.toLowerCase()).not.toContain('world', {});
+console.log(safe.toString()); // "Hello world
"
+```
+
+### Allow extra tags / attributes
+
+```ts
+import { Utils } from '@formio/js/utils';
+
+const safe = Utils.sanitize('Hi', {
+ addTags: ['article'],
+ addAttr: ['data-tag'],
+});
+```
+
+### Manipulate the DOM via helpers (from `@formio/core`)
+
+```ts
+import { dom } from '@formio/core';
+
+const banner = document.createElement('div');
+banner.className = 'banner';
+banner.textContent = 'Saved!';
+dom.prependTo(banner, document.getElementById('formio')!);
+
+setTimeout(() => {
+ dom.removeChildFrom(banner, document.getElementById('formio')!);
+}, 2000);
+```
diff --git a/plugin/skills/formio-sdk/references/utils-misc.md b/plugin/skills/formio-sdk/references/utils-misc.md
new file mode 100644
index 0000000..672e420
--- /dev/null
+++ b/plugin/skills/formio-sdk/references/utils-misc.md
@@ -0,0 +1,160 @@
+## Overview
+
+Miscellaneous helpers — date utilities, i18n, JWT decode, submission unwind, deep clone, and class override. Sourced from `packages/core/src/utils/date.ts`, `packages/core/src/utils/i18n.ts`, `packages/core/src/utils/unwind.ts`, `packages/core/src/utils/fastCloneDeep.ts`, and `packages/core/src/utils/override.ts` in the Form.io source code.
+
+`@formio/js/utils` re-exports the date helpers and `fastCloneDeep` flat on `Utils`. The renderer does **not** re-export `I18n`, `unwind`, or `override` — import those from `@formio/core`. There is no `jwtDecode` helper on either entry point: use `Formio.getToken({ decode: true })` to read the cached SDK JWT, or pull a standalone decoder for arbitrary tokens.
+
+## Imports
+
+```ts
+import { Utils } from '@formio/js/utils'; // date helpers, fastCloneDeep
+import { I18n, unwind, override } from '@formio/core'; // not exposed by @formio/js
+import { Formio } from '@formio/js'; // for JWT decode via getToken({ decode: true })
+```
+
+## API
+
+Date (flat on `Utils`):
+
+- `Utils.moment` — the bundled `moment` instance with `moment-timezone` loaded (`.tz()` is available).
+- `Utils.momentDate(date, format?, timezone?): moment.Moment` — convenience constructor that respects a timezone string.
+- `Utils.currentTimezone(): string` — browser/Node timezone via `moment.tz.guess()`.
+- `Utils.convertFormatToMoment(format: string): string` — translate Angular date format tokens (`MM/dd/yyyy`) into moment tokens (`MM/DD/YYYY`).
+- `Utils.formatDate(value, format, timezone?): string` — high-level formatter used by Form.io's Date/Time component.
+- `Utils.isValidDate(value): boolean`, `Utils.offsetDate(date, timezone?)`, `Utils.getLocaleDateFormatInfo(locale?)`, `Utils.getDateSetting(value)` — additional helpers re-exported flat on `Utils`.
+
+i18n (`I18n` class from `@formio/core`):
+
+- `new I18n(languages?: Record>)` — manage a language dictionary at runtime.
+- `setLanguages(languages)` — install or replace dictionaries.
+- `changeLanguage(language)` — switch active language.
+- `t(key, defaultValue?)` — translate a key under the active language.
+
+In the renderer, the live translator is exposed on the `Form` instance as `form.i18next` (i18next-backed); the `I18n` class is the standalone dictionary helper used outside the renderer.
+
+JWT decode:
+
+- `Formio.getToken({ decode: true })` — read **and** decode the SDK's currently-cached JWT in one call. Returns the decoded payload (`{ user, form, project, exp, iat, … }`).
+- For decoding arbitrary JWTs (not the SDK's cached one), pull a standalone decoder such as the `jwt-decode` npm package. `@formio/core` ships an internal `jwtDecode` but does not re-export it from any public entry point.
+
+Submission unwind (`unwind` from `@formio/core`):
+
+- `unwind(form, submission): Submission[]` — explode nested array data into one submission per row. Useful for exporting datagrid/editgrid rows as flat records. `rewind` is **not** part of the public surface; use a manual fold or `lodash.merge` to reassemble.
+
+Cloning and override (mixed):
+
+- `Utils.fastCloneDeep(obj: any): any` — `JSON.parse(JSON.stringify(obj))` with error handling; returns `null` on failure.
+- `override(classObj: any, extenders: any): void` (from `@formio/core`) — replace prototype methods/properties on a class. Each entry in `extenders` is either a function (replaces the method) or a property descriptor.
+
+## Examples
+
+### Decode the SDK's cached JWT
+
+```ts
+import { Formio } from '@formio/js';
+
+const claims = Formio.getToken({ decode: true });
+if (claims && claims.user) {
+ console.log('logged in as', claims.user._id, 'expires', claims.exp);
+}
+```
+
+### Translate an Angular date format
+
+```ts
+import { Utils } from '@formio/js/utils';
+
+const momentFormat = Utils.convertFormatToMoment('MM/dd/yyyy h:mm a');
+console.log(momentFormat); // "MM/DD/YYYY h:mm A"
+```
+
+### Format the current time in the browser's timezone
+
+```ts
+import { Utils } from '@formio/js/utils';
+
+const now = Utils.moment().tz(Utils.currentTimezone()).format('YYYY-MM-DD HH:mm');
+console.log(now);
+```
+
+### Switch language at runtime via `I18n` (from `@formio/core`)
+
+```ts
+import { I18n } from '@formio/core';
+
+const i18n = new I18n();
+i18n.setLanguages({
+ en: { hello: 'Hello' },
+ fr: { hello: 'Bonjour' },
+});
+i18n.changeLanguage('fr');
+console.log(i18n.t('hello')); // "Bonjour"
+```
+
+### Deep-clone a submission safely
+
+```ts
+import { Utils } from '@formio/js/utils';
+
+const draft = Utils.fastCloneDeep(submission);
+if (draft) {
+ draft.data.firstName = 'Edited';
+}
+```
+
+### Override a component class method (`override` from `@formio/core`)
+
+```ts
+import { override } from '@formio/core';
+
+class TextFieldStub {
+ rawValue = 'raw ';
+ getValue() {
+ return this.rawValue;
+ }
+}
+
+override(TextFieldStub, {
+ getValue(this: TextFieldStub) {
+ const v = (this as unknown as { _origGetValue: () => string })._origGetValue();
+ return typeof v === 'string' ? v.trim() : v;
+ },
+ _origGetValue: TextFieldStub.prototype.getValue,
+});
+
+console.log(new TextFieldStub().getValue()); // "raw"
+```
+
+### Unwind a submission with a nested array (`unwind` from `@formio/core`)
+
+```ts
+import { unwind } from '@formio/core';
+
+const form = {
+ components: [
+ { type: 'textfield', key: 'customer', input: true },
+ {
+ type: 'datagrid',
+ key: 'items',
+ input: true,
+ components: [
+ { type: 'textfield', key: 'sku', input: true },
+ { type: 'number', key: 'qty', input: true },
+ ],
+ },
+ ],
+};
+
+const submission = {
+ data: {
+ customer: 'Acme',
+ items: [
+ { sku: 'A1', qty: 2 },
+ { sku: 'B2', qty: 5 },
+ ],
+ },
+};
+
+const rows = unwind(form, submission);
+console.log(rows.length); // one submission per top-level row in the unwound output
+```
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2db5d6f..81b3962 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -71,13 +71,37 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.1.1
- version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)
+ version: 3.2.4(@types/node@22.19.17)(happy-dom@15.11.7)(jsdom@25.0.1)(tsx@4.21.0)
+
+ packages/skill-tests:
+ devDependencies:
+ '@formio/core':
+ specifier: ^2.6.6
+ version: 2.6.6
+ '@formio/js':
+ specifier: ^5.3.6
+ version: 5.3.6(@popperjs/core@2.11.8)
+ '@types/node':
+ specifier: ^22.19.17
+ version: 22.19.17
+ jsdom:
+ specifier: ^25.0.1
+ version: 25.0.1
+ typescript:
+ specifier: ^5.8.2
+ version: 5.9.3
+ vitest:
+ specifier: ^3.1.1
+ version: 3.2.4(@types/node@22.19.17)(happy-dom@15.11.7)(jsdom@25.0.1)(tsx@4.21.0)
plugin:
publishDirectory: ../dist/plugin
packages:
+ '@asamuzakjp/css-color@3.2.0':
+ resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
+
'@babel/runtime@7.29.2':
resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==}
engines: {node: '>=6.9.0'}
@@ -137,6 +161,34 @@ packages:
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
+ '@csstools/color-helpers@5.1.0':
+ resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
+ engines: {node: '>=18'}
+
+ '@csstools/css-calc@2.1.4':
+ resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-color-parser@3.1.0':
+ resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-parser-algorithms': ^3.0.5
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5':
+ resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ '@csstools/css-tokenizer': ^3.0.4
+
+ '@csstools/css-tokenizer@3.0.4':
+ resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
+ engines: {node: '>=18'}
+
'@esbuild/aix-ppc64@0.27.7':
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
engines: {node: '>=18'}
@@ -331,6 +383,21 @@ packages:
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@formio/bootstrap@3.2.2':
+ resolution: {integrity: sha512-ZKcec6BACYS1oG+cto+SIY4VJMRPwDbtVJQdPzoMjOwsBXPGhgtnZoPL0mx3rdGw8BS/aGHoOFBoFQbmsKVBuA==}
+
+ '@formio/core@2.6.6':
+ resolution: {integrity: sha512-pbkUm3UlztfJlU/YFTH1YrD0n7uq/B15EL0SU/G8EzaFtersoJqm6T8eA2mTpH8dp0Sygf2QpWg3aVv6nsO76Q==}
+
+ '@formio/js@5.3.6':
+ resolution: {integrity: sha512-GyAIX08N/p1eAgJrnTRHcpkAms5Mr7rFr8d43c+CM8V60jGGIAH7pSNECLbrdBk6SFYVVsE2aMSFBDAU8rfMTg==}
+
+ '@formio/text-mask-addons@3.8.0-formio.4':
+ resolution: {integrity: sha512-vhkeIyuL+1rtC9S4IW8O3JCwroPtvJrkrcMO4wyELNqMIgQRKbiyBAitZfUP4tY04xdB5lxAinbzdwb+NMdX6w==}
+
+ '@formio/vanilla-text-mask@5.1.1-formio.1':
+ resolution: {integrity: sha512-rYBlvIPMNUd6sAaduOaiIwI4vfTAjHDRonko2qJn2RP1O//TQ7rcFIPYVYePJZ4OtOpwHiHAvAIh79McphZotQ==}
+
'@hono/node-server@1.19.13':
resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==}
engines: {node: '>=18.14.1'}
@@ -393,6 +460,9 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@popperjs/core@2.11.8':
+ resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+
'@rollup/rollup-android-arm-eabi@4.60.1':
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
cpu: [arm]
@@ -531,6 +601,9 @@ packages:
cpu: [x64]
os: [win32]
+ '@sphinxxxx/color-conversion@2.2.2':
+ resolution: {integrity: sha512-XExJS3cLqgrmNBIP3bBw6+1oQ1ksGjFh0+oClDKFYpCCqx/hlqwWO5KO/S63fzUo67SxI9dMrF0y5T/Ey7h8Zw==}
+
'@turbo/darwin-64@2.9.5':
resolution: {integrity: sha512-qPxhKsLMQP+9+dsmPgAGidi5uNifD4AoAOnEnljab3Qgn0QZRR31Hp+/CgW3Ia5AanWj6JuLLTBYvuQj4mqTWg==}
cpu: [x64]
@@ -612,6 +685,9 @@ packages:
'@types/serve-static@1.15.10':
resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==}
+ '@types/trusted-types@2.0.7':
+ resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
+
'@typescript-eslint/eslint-plugin@8.58.1':
resolution: {integrity: sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -700,6 +776,9 @@ packages:
'@vitest/utils@3.2.4':
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
+ abortcontroller-polyfill@1.7.8:
+ resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==}
+
accepts@1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
@@ -718,6 +797,10 @@ packages:
engines: {node: '>=0.4.0'}
hasBin: true
+ agent-base@7.1.4:
+ resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
+ engines: {node: '>= 14'}
+
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
@@ -732,6 +815,9 @@ packages:
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
+ animation-frame-polyfill@1.1.0:
+ resolution: {integrity: sha512-ix9fY7tjhq+MLO/sBltxWzJHET+KWBgir2IOcEkFTcsoHH5a64c8gqe90+PS+qMSgfXd9PG5BAjrANvX12/Ckw==}
+
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
@@ -753,6 +839,9 @@ packages:
array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
+ array-from@2.1.1:
+ resolution: {integrity: sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==}
+
array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@@ -761,6 +850,15 @@ packages:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
+ asynckit@0.4.0:
+ resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+ atoa@1.0.0:
+ resolution: {integrity: sha512-VVE1H6cc4ai+ZXo/CRWoJiHXrA1qfA31DPnx6D20+kSI547hQN5Greh51LQ1baMRMfxO5K5M4ImMtZbZt2DODQ==}
+
+ autocompleter@8.0.4:
+ resolution: {integrity: sha512-q334YswwucSBphN5djVkEt3beVhHotrCtPGNIXmyilw9UnXV9Cb+gNAZ2yhZSfiBSzP6rxHLLT2gpr57xgbcwQ==}
+
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -780,6 +878,11 @@ packages:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
+ bootstrap@5.3.8:
+ resolution: {integrity: sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==}
+ peerDependencies:
+ '@popperjs/core': ^2.11.8
+
brace-expansion@1.1.13:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
@@ -791,6 +894,12 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ browser-cookies@1.2.0:
+ resolution: {integrity: sha512-cg2WuoOJo+F+g2XjEaP8nmeRp1vDHjt7sqpKJMsTNXKrpyIBNVslYJeehvs6FEddj8usV2+qyRSBEX244yN5/g==}
+
+ browser-md5-file@1.1.1:
+ resolution: {integrity: sha512-9h2UViTtZPhBa7oHvp5mb7MvJaX5OKEPUsplDwJ800OIV+In7BOR3RXOMB78obn2iQVIiS3WkVLhG7Zu1EMwbw==}
+
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@@ -826,6 +935,9 @@ packages:
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
engines: {node: '>= 16'}
+ choices.js@11.2.3:
+ resolution: {integrity: sha512-K+HA8ShsWWNOVs4ahahjIOtzsvcC/cOFGTdU1BZgMZRhGi/uCQARlFd/80B0hlVVdRIjnrsW7QLDUnpJaW6N7A==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -833,6 +945,13 @@ packages:
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ combined-stream@1.0.8:
+ resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+ engines: {node: '>= 0.8'}
+
+ compare-versions@6.1.1:
+ resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -848,6 +967,9 @@ packages:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
+ contra@1.9.4:
+ resolution: {integrity: sha512-N9ArHAqwR/lhPq4OdIAwH4e1btn6EIZMAz4TazjnzCiVECcWUPTma+dRAM38ERImEJBh8NiCCpjoQruSZ+agYg==}
+
cookie-signature@1.0.7:
resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==}
@@ -859,14 +981,37 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
+ core-js@3.49.0:
+ resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==}
+
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
+ create-point-cb@1.2.0:
+ resolution: {integrity: sha512-r4l6IO/YGI7hIZRMLggOzwM6XO80+Fdcv4hx1fXCEdU+hKd7zZki6i+cbYfK9OliMwMYx1wPfQLU/snvS+Dygw==}
+
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
+ crossvent@1.5.5:
+ resolution: {integrity: sha512-MY4xhBYEnVi+pmTpHCOCsCLYczc0PVtGdPBz6NXNXxikLaUZo4HdAeUb1UqAo3t3yXAloSelTmfxJ+/oUqkW5w==}
+
+ cssstyle@4.6.0:
+ resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
+ engines: {node: '>=18'}
+
+ custom-event@1.0.1:
+ resolution: {integrity: sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==}
+
+ data-urls@5.0.0:
+ resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
+ engines: {node: '>=18'}
+
+ dayjs@1.11.21:
+ resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
+
debug@2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
peerDependencies:
@@ -884,6 +1029,9 @@ packages:
supports-color:
optional: true
+ decimal.js@10.6.0:
+ resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
+
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@@ -891,6 +1039,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+ delayed-stream@1.0.0:
+ resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+ engines: {node: '>=0.4.0'}
+
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@@ -903,10 +1055,34 @@ packages:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
+ dialog-polyfill@0.5.6:
+ resolution: {integrity: sha512-ZbVDJI9uvxPAKze6z146rmfUZjBqNEwcnFTVamQzXH+svluiV7swmVIGr7miwADgfgt1G2JQIytypM9fbyhX4w==}
+
dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
+ dom-autoscroller@2.3.4:
+ resolution: {integrity: sha512-HcAdt/2Dq9x4CG6LWXc2x9Iq0MJPAu8fuzHncclq7byufqYEYVtx9sZ/dyzR+gdj4qwEC9p27Lw1G2HRRYX6jQ==}
+
+ dom-mousemove-dispatcher@1.0.1:
+ resolution: {integrity: sha512-NMdqqMbgW8kqOdmod2hkS+9hD/v7h4XoSvwU9qqe+wAA/O+ba0jhpbfW0Kb/fCyR0RX9jf4dwfQrl04LQX4FzQ==}
+
+ dom-plane@1.0.2:
+ resolution: {integrity: sha512-/tR67G6ZGSciXoZLsD706yLxEXvX3mG/OWE8YNYj3A1yU/RAimtPXzklVTu5Y5xoeMoloA/Y+MaNjQm9apgAww==}
+
+ dom-set@1.1.1:
+ resolution: {integrity: sha512-sUi2aSvRsK3Ixx++gwX9cnaWk9ZxGVFry8+HnTRVmDimybU5PaiI4wX0o00mVtjFKlQNZLmtGoPTLorYbN0+Rw==}
+
+ dompurify@3.4.7:
+ resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==}
+
+ downloadjs@1.4.7:
+ resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==}
+
+ dragula@3.7.3:
+ resolution: {integrity: sha512-/rRg4zRhcpf81TyDhaHLtXt6sEywdfpv1cRUMeFFy7DuypH2U0WUL0GTdyAQvXegviT4PJK4KuMmOaIDpICseQ==}
+
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
@@ -922,6 +1098,14 @@ packages:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ entities@6.0.1:
+ resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
+ engines: {node: '>=0.12'}
+
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -937,6 +1121,10 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
+ es-set-tostringtag@2.1.0:
+ resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+ engines: {node: '>= 0.4'}
+
esbuild@0.27.7:
resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
engines: {node: '>=18'}
@@ -1013,6 +1201,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
@@ -1049,10 +1240,16 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ fast-diff@1.3.0:
+ resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
+
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
+ fast-json-patch@3.1.1:
+ resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==}
+
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@@ -1074,6 +1271,9 @@ packages:
picomatch:
optional: true
+ fetch-ponyfill@7.1.0:
+ resolution: {integrity: sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==}
+
file-entry-cache@8.0.0:
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
engines: {node: '>=16.0.0'}
@@ -1105,6 +1305,10 @@ packages:
flatted@3.4.2:
resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==}
+ form-data@4.0.5:
+ resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+ engines: {node: '>= 6'}
+
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
@@ -1133,6 +1337,10 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+ fuse.js@7.3.0:
+ resolution: {integrity: sha512-plz8RVjfcDedTGfVngWH1jmJvBvAwi1v2jecfDerbEnMcmOYUEEwKFTHbNoCiYyzaK2Ws8lABkTCcRSqCY1q4w==}
+ engines: {node: '>=10'}
+
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@@ -1171,6 +1379,10 @@ packages:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
+ happy-dom@15.11.7:
+ resolution: {integrity: sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==}
+ engines: {node: '>=18.0.0'}
+
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
@@ -1179,6 +1391,10 @@ packages:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
+ has-tostringtag@1.0.2:
+ resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+ engines: {node: '>= 0.4'}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -1187,10 +1403,22 @@ packages:
resolution: {integrity: sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==}
engines: {node: '>=16.9.0'}
+ html-encoding-sniffer@4.0.0:
+ resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
+ engines: {node: '>=18'}
+
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
+ http-proxy-agent@7.0.2:
+ resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
+ engines: {node: '>= 14'}
+
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
human-id@4.1.3:
resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==}
hasBin: true
@@ -1204,10 +1432,17 @@ packages:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
+ iconv-lite@0.6.3:
+ resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
+ engines: {node: '>=0.10.0'}
+
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
+ idb@7.1.1:
+ resolution: {integrity: sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==}
+
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@@ -1227,6 +1462,9 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+ inputmask@5.0.9:
+ resolution: {integrity: sha512-s0lUfqcEbel+EQXtehXqwCJGShutgieOaIImFKC/r4reYNvX3foyrChl6LOEvaEgxEbesePIrw1Zi2jhZaDZbQ==}
+
ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
@@ -1235,6 +1473,9 @@ packages:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
+ is-array@1.0.1:
+ resolution: {integrity: sha512-gxiZ+y/u67AzpeFmAmo4CbtME/bs7J2C++su5zQzvQyaxUqVzkh69DI+jN+KZuSO6JaH6TIIU6M6LhqxMjxEpw==}
+
is-extendable@0.1.1:
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
engines: {node: '>=0.10.0'}
@@ -1251,6 +1492,9 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
+ is-potential-custom-element-name@1.0.1:
+ resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
+
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@@ -1262,9 +1506,15 @@ packages:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
+ iselement@1.1.4:
+ resolution: {integrity: sha512-4Q519eWmbHO1pbimiz7H1iJRUHVmAmfh0viSsUD+oAwVO4ntZt7gpf8i8AShVBTyOvRTZNYNBpUxOIvwZR+ffw==}
+
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ ismobilejs@1.1.1:
+ resolution: {integrity: sha512-VaFW53yt8QO61k2WJui0dHf4SlL8lxBofUuUmwBo0ljPk0Drz2TiuDW4jo3wDcv41qy/SxrJ+VAzJ/qYqsmzRw==}
+
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
@@ -1279,9 +1529,21 @@ packages:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
+ jsdom@25.0.1:
+ resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ canvas: ^2.11.2
+ peerDependenciesMeta:
+ canvas:
+ optional: true
+
json-buffer@3.0.1:
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+ json-logic-js@2.0.5:
+ resolution: {integrity: sha512-rTT2+lqcuUmj4DgWfmzupZqQDA64AdmYqizzMPWj3DxGdfFNsxPpcNVSaTj4l8W2tG/+hg7/mQhxjU3aPacO6g==}
+
json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@@ -1297,6 +1559,12 @@ packages:
jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
+ jstimezonedetect@1.0.7:
+ resolution: {integrity: sha512-ARADHortktl9IZ1tr4GHwGPIAzgz3mLNCbR/YjWtRtc/O0o634O3NeFlpLjv95EvuDA5dc8z6yfgbS8nUc4zcQ==}
+
+ jwt-decode@3.1.2:
+ resolution: {integrity: sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==}
+
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1316,15 +1584,31 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
+ lodash-es@4.18.1:
+ resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
+
+ lodash.clonedeep@4.5.0:
+ resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
+
+ lodash.isequal@4.5.0:
+ resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
+ deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
+
lodash.merge@4.6.2:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
+ lodash@4.18.1:
+ resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
+
loupe@3.2.1:
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+ lru-cache@10.4.3:
+ resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
+
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
@@ -1387,6 +1671,12 @@ packages:
minimatch@3.1.5:
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
+ moment-timezone@0.5.48:
+ resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
+
+ moment@2.30.1:
+ resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -1413,6 +1703,18 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
+ node-fetch@2.6.13:
+ resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+
+ nwsapi@2.2.23:
+ resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==}
+
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -1466,10 +1768,16 @@ packages:
package-manager-detector@0.2.11:
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
+ parchment@3.0.0:
+ resolution: {integrity: sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==}
+
parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'}
+ parse5@7.3.0:
+ resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
+
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
@@ -1558,6 +1866,14 @@ packages:
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+ quill-delta@5.1.0:
+ resolution: {integrity: sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==}
+ engines: {node: '>= 12.0.0'}
+
+ quill@2.0.3:
+ resolution: {integrity: sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==}
+ engines: {npm: '>=8.2.3'}
+
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
@@ -1602,6 +1918,12 @@ packages:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
+ rrweb-cssom@0.7.1:
+ resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==}
+
+ rrweb-cssom@0.8.0:
+ resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==}
+
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -1611,6 +1933,10 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ saxes@6.0.0:
+ resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
+ engines: {node: '>=v12.22.7'}
+
section-matter@1.0.0:
resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==}
engines: {node: '>=4'}
@@ -1670,6 +1996,9 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
+ signature_pad@4.2.0:
+ resolution: {integrity: sha512-YLWysmaUBaC5wosAKkgbX7XI+LBv2w5L0QUcI6Jc4moHYzv9BUBJtAyNLpWzHjtjKTeWOH6bfP4a4pzf0UinfQ==}
+
slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@@ -1678,6 +2007,9 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
+ spark-md5@2.0.2:
+ resolution: {integrity: sha512-9WfT+FYBEvlrOOBEs484/zmbtSX4BlGjzXih1qIEWA1yhHbcqgcMHkiwXoWk2Sq1aJjLpcs6ZKV7JxrDNjIlNg==}
+
spawndamnit@3.0.1:
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
@@ -1694,6 +2026,9 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
+ string-hash@1.1.3:
+ resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
+
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@@ -1717,10 +2052,16 @@ packages:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
+ symbol-tree@3.2.4:
+ resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+
term-size@2.2.1:
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
engines: {node: '>=8'}
+ ticky@1.0.1:
+ resolution: {integrity: sha512-RX35iq/D+lrsqhcPWIazM9ELkjOe30MSeoBHQHSsRwd1YuhJO5ui1K1/R0r7N3mFvbLBs33idw+eR6j+w6i/DA==}
+
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -1743,6 +2084,16 @@ packages:
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
engines: {node: '>=14.0.0'}
+ tippy.js@6.3.7:
+ resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==}
+
+ tldts-core@6.1.86:
+ resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
+
+ tldts@6.1.86:
+ resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
+ hasBin: true
+
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -1751,6 +2102,17 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
+ tough-cookie@5.1.2:
+ resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
+ engines: {node: '>=16'}
+
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
+ tr46@5.1.1:
+ resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
+ engines: {node: '>=18'}
+
ts-api-utils@2.5.0:
resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==}
engines: {node: '>=18.12'}
@@ -1770,6 +2132,9 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
+ type-func@1.0.3:
+ resolution: {integrity: sha512-YA90CUk+i00tWESPNRMahywXhAz+12NLJLKlOWrgHIbqaFXjdZrWstRghaibOW/IxhPjui4SmXxO/03XSGRIjA==}
+
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@@ -1808,6 +2173,14 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
+ uuid@9.0.1:
+ resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
+ hasBin: true
+
+ vanilla-picker@2.12.3:
+ resolution: {integrity: sha512-qVkT1E7yMbUsB2mmJNFmaXMWE2hF8ffqzMMwe9zdAikd8u2VfnsVY2HQcOUi2F38bgbxzlJBEdS1UUhOXdF9GQ==}
+
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -1885,6 +2258,37 @@ packages:
jsdom:
optional: true
+ w3c-xmlserializer@5.0.0:
+ resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
+ engines: {node: '>=18'}
+
+ webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+
+ webidl-conversions@7.0.0:
+ resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
+ engines: {node: '>=12'}
+
+ whatwg-encoding@3.1.1:
+ resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
+ engines: {node: '>=18'}
+ deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
+
+ whatwg-mimetype@3.0.0:
+ resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
+ engines: {node: '>=12'}
+
+ whatwg-mimetype@4.0.0:
+ resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
+ engines: {node: '>=18'}
+
+ whatwg-url@14.2.0:
+ resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
+ engines: {node: '>=18'}
+
+ whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@@ -1902,6 +2306,25 @@ packages:
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
+ ws@8.21.0:
+ resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+
+ xml-name-validator@5.0.0:
+ resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
+ engines: {node: '>=18'}
+
+ xmlchars@2.2.0:
+ resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -1916,6 +2339,14 @@ packages:
snapshots:
+ '@asamuzakjp/css-color@3.2.0':
+ dependencies:
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+ lru-cache: 10.4.3
+
'@babel/runtime@7.29.2': {}
'@changesets/apply-release-plan@7.1.0':
@@ -2061,6 +2492,26 @@ snapshots:
human-id: 4.1.3
prettier: 2.8.8
+ '@csstools/color-helpers@5.1.0': {}
+
+ '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/color-helpers': 5.1.0
+ '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4)
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)':
+ dependencies:
+ '@csstools/css-tokenizer': 3.0.4
+
+ '@csstools/css-tokenizer@3.0.4': {}
+
'@esbuild/aix-ppc64@0.27.7':
optional: true
@@ -2185,6 +2636,69 @@ snapshots:
'@eslint/core': 0.17.0
levn: 0.4.1
+ '@formio/bootstrap@3.2.2': {}
+
+ '@formio/core@2.6.6':
+ dependencies:
+ browser-cookies: 1.2.0
+ core-js: 3.49.0
+ dayjs: 1.11.21
+ dompurify: 3.4.7
+ eventemitter3: 5.0.4
+ fast-json-patch: 3.1.1
+ fetch-ponyfill: 7.1.0
+ inputmask: 5.0.9
+ json-logic-js: 2.0.5
+ lodash: 4.18.1
+ moment: 2.30.1
+ transitivePeerDependencies:
+ - encoding
+
+ '@formio/js@5.3.6(@popperjs/core@2.11.8)':
+ dependencies:
+ '@formio/bootstrap': 3.2.2
+ '@formio/core': 2.6.6
+ '@formio/text-mask-addons': 3.8.0-formio.4
+ '@formio/vanilla-text-mask': 5.1.1-formio.1
+ abortcontroller-polyfill: 1.7.8
+ autocompleter: 8.0.4
+ bootstrap: 5.3.8(@popperjs/core@2.11.8)
+ browser-cookies: 1.2.0
+ browser-md5-file: 1.1.1
+ choices.js: 11.2.3
+ compare-versions: 6.1.1
+ core-js: 3.49.0
+ dialog-polyfill: 0.5.6
+ dom-autoscroller: 2.3.4
+ dompurify: 3.4.7
+ downloadjs: 1.4.7
+ dragula: 3.7.3
+ eventemitter3: 5.0.4
+ fast-deep-equal: 3.1.3
+ fast-json-patch: 3.1.1
+ idb: 7.1.1
+ inputmask: 5.0.9
+ ismobilejs: 1.1.1
+ json-logic-js: 2.0.5
+ jstimezonedetect: 1.0.7
+ jwt-decode: 3.1.2
+ lodash: 4.18.1
+ moment: 2.30.1
+ moment-timezone: 0.5.48
+ quill: 2.0.3
+ signature_pad: 4.2.0
+ string-hash: 1.1.3
+ tippy.js: 6.3.7
+ uuid: 9.0.1
+ vanilla-picker: 2.12.3
+ transitivePeerDependencies:
+ - '@popperjs/core'
+ - encoding
+
+ '@formio/text-mask-addons@3.8.0-formio.4': {}
+
+ '@formio/vanilla-text-mask@5.1.1-formio.1': {}
+
'@hono/node-server@1.19.13(hono@4.12.12)':
dependencies:
hono: 4.12.12
@@ -2259,6 +2773,8 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
+ '@popperjs/core@2.11.8': {}
+
'@rollup/rollup-android-arm-eabi@4.60.1':
optional: true
@@ -2334,6 +2850,8 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.60.1':
optional: true
+ '@sphinxxxx/color-conversion@2.2.2': {}
+
'@turbo/darwin-64@2.9.5':
optional: true
@@ -2415,6 +2933,9 @@ snapshots:
'@types/node': 22.19.17
'@types/send': 0.17.6
+ '@types/trusted-types@2.0.7':
+ optional: true
+
'@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
@@ -2548,6 +3069,8 @@ snapshots:
loupe: 3.2.1
tinyrainbow: 2.0.0
+ abortcontroller-polyfill@1.7.8: {}
+
accepts@1.3.8:
dependencies:
mime-types: 2.1.35
@@ -2564,6 +3087,8 @@ snapshots:
acorn@8.16.0: {}
+ agent-base@7.1.4: {}
+
ajv-formats@3.0.1(ajv@8.18.0):
optionalDependencies:
ajv: 8.18.0
@@ -2582,6 +3107,8 @@ snapshots:
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
+ animation-frame-polyfill@1.1.0: {}
+
ansi-colors@4.1.3: {}
ansi-regex@5.0.1: {}
@@ -2598,10 +3125,18 @@ snapshots:
array-flatten@1.1.1: {}
+ array-from@2.1.1: {}
+
array-union@2.1.0: {}
assertion-error@2.0.1: {}
+ asynckit@0.4.0: {}
+
+ atoa@1.0.0: {}
+
+ autocompleter@8.0.4: {}
+
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
@@ -2641,6 +3176,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ bootstrap@5.3.8(@popperjs/core@2.11.8):
+ dependencies:
+ '@popperjs/core': 2.11.8
+
brace-expansion@1.1.13:
dependencies:
balanced-match: 1.0.2
@@ -2654,6 +3193,12 @@ snapshots:
dependencies:
fill-range: 7.1.1
+ browser-cookies@1.2.0: {}
+
+ browser-md5-file@1.1.1:
+ dependencies:
+ spark-md5: 2.0.2
+
bytes@3.1.2: {}
cac@6.7.14: {}
@@ -2687,12 +3232,22 @@ snapshots:
check-error@2.1.3: {}
+ choices.js@11.2.3:
+ dependencies:
+ fuse.js: 7.3.0
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.4: {}
+ combined-stream@1.0.8:
+ dependencies:
+ delayed-stream: 1.0.0
+
+ compare-versions@6.1.1: {}
+
concat-map@0.0.1: {}
content-disposition@0.5.4:
@@ -2703,23 +3258,52 @@ snapshots:
content-type@1.0.5: {}
+ contra@1.9.4:
+ dependencies:
+ atoa: 1.0.0
+ ticky: 1.0.1
+
cookie-signature@1.0.7: {}
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
+ core-js@3.49.0: {}
+
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
+ create-point-cb@1.2.0:
+ dependencies:
+ type-func: 1.0.3
+
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
+ crossvent@1.5.5:
+ dependencies:
+ custom-event: 1.0.1
+
+ cssstyle@4.6.0:
+ dependencies:
+ '@asamuzakjp/css-color': 3.2.0
+ rrweb-cssom: 0.8.0
+
+ custom-event@1.0.1: {}
+
+ data-urls@5.0.0:
+ dependencies:
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+
+ dayjs@1.11.21: {}
+
debug@2.6.9:
dependencies:
ms: 2.0.0
@@ -2728,20 +3312,58 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js@10.6.0: {}
+
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
+ delayed-stream@1.0.0: {}
+
depd@2.0.0: {}
destroy@1.2.0: {}
detect-indent@6.1.0: {}
+ dialog-polyfill@0.5.6: {}
+
dir-glob@3.0.1:
dependencies:
path-type: 4.0.0
+ dom-autoscroller@2.3.4:
+ dependencies:
+ animation-frame-polyfill: 1.1.0
+ create-point-cb: 1.2.0
+ dom-mousemove-dispatcher: 1.0.1
+ dom-plane: 1.0.2
+ dom-set: 1.1.1
+ type-func: 1.0.3
+
+ dom-mousemove-dispatcher@1.0.1: {}
+
+ dom-plane@1.0.2:
+ dependencies:
+ create-point-cb: 1.2.0
+
+ dom-set@1.1.1:
+ dependencies:
+ array-from: 2.1.1
+ is-array: 1.0.1
+ iselement: 1.1.4
+
+ dompurify@3.4.7:
+ optionalDependencies:
+ '@types/trusted-types': 2.0.7
+
+ downloadjs@1.4.7: {}
+
+ dragula@3.7.3:
+ dependencies:
+ contra: 1.9.4
+ crossvent: 1.5.5
+
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -2757,6 +3379,11 @@ snapshots:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
+ entities@4.5.0:
+ optional: true
+
+ entities@6.0.1: {}
+
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -2767,6 +3394,13 @@ snapshots:
dependencies:
es-errors: 1.3.0
+ es-set-tostringtag@2.1.0:
+ dependencies:
+ es-errors: 1.3.0
+ get-intrinsic: 1.3.0
+ has-tostringtag: 1.0.2
+ hasown: 2.0.2
+
esbuild@0.27.7:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.7
@@ -2880,6 +3514,8 @@ snapshots:
etag@1.8.1: {}
+ eventemitter3@5.0.4: {}
+
eventsource-parser@3.0.6: {}
eventsource@3.0.7:
@@ -2970,6 +3606,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
+ fast-diff@1.3.0: {}
+
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
@@ -2978,6 +3616,8 @@ snapshots:
merge2: 1.4.1
micromatch: 4.0.8
+ fast-json-patch@3.1.1: {}
+
fast-json-stable-stringify@2.1.0: {}
fast-levenshtein@2.0.6: {}
@@ -2992,6 +3632,12 @@ snapshots:
optionalDependencies:
picomatch: 4.0.4
+ fetch-ponyfill@7.1.0:
+ dependencies:
+ node-fetch: 2.6.13
+ transitivePeerDependencies:
+ - encoding
+
file-entry-cache@8.0.0:
dependencies:
flat-cache: 4.0.1
@@ -3040,6 +3686,14 @@ snapshots:
flatted@3.4.2: {}
+ form-data@4.0.5:
+ dependencies:
+ asynckit: 0.4.0
+ combined-stream: 1.0.8
+ es-set-tostringtag: 2.1.0
+ hasown: 2.0.2
+ mime-types: 2.1.35
+
forwarded@0.2.0: {}
fresh@0.5.2: {}
@@ -3063,6 +3717,8 @@ snapshots:
function-bind@1.1.2: {}
+ fuse.js@7.3.0: {}
+
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@@ -3115,16 +3771,31 @@ snapshots:
section-matter: 1.0.0
strip-bom-string: 1.0.0
+ happy-dom@15.11.7:
+ dependencies:
+ entities: 4.5.0
+ webidl-conversions: 7.0.0
+ whatwg-mimetype: 3.0.0
+ optional: true
+
has-flag@4.0.0: {}
has-symbols@1.1.0: {}
+ has-tostringtag@1.0.2:
+ dependencies:
+ has-symbols: 1.1.0
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
hono@4.12.12: {}
+ html-encoding-sniffer@4.0.0:
+ dependencies:
+ whatwg-encoding: 3.1.1
+
http-errors@2.0.1:
dependencies:
depd: 2.0.0
@@ -3133,6 +3804,20 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
+ http-proxy-agent@7.0.2:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ https-proxy-agent@7.0.6:
+ dependencies:
+ agent-base: 7.1.4
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
human-id@4.1.3: {}
husky@9.1.7: {}
@@ -3141,10 +3826,16 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
+ iconv-lite@0.6.3:
+ dependencies:
+ safer-buffer: 2.1.2
+
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
+ idb@7.1.1: {}
+
ignore@5.3.2: {}
ignore@7.0.5: {}
@@ -3158,10 +3849,14 @@ snapshots:
inherits@2.0.4: {}
+ inputmask@5.0.9: {}
+
ip-address@10.1.0: {}
ipaddr.js@1.9.1: {}
+ is-array@1.0.1: {}
+
is-extendable@0.1.1: {}
is-extglob@2.1.1: {}
@@ -3172,6 +3867,8 @@ snapshots:
is-number@7.0.0: {}
+ is-potential-custom-element-name@1.0.1: {}
+
is-promise@4.0.0: {}
is-subdir@1.2.0:
@@ -3180,8 +3877,12 @@ snapshots:
is-windows@1.0.2: {}
+ iselement@1.1.4: {}
+
isexe@2.0.0: {}
+ ismobilejs@1.1.1: {}
+
jose@6.2.2: {}
js-tokens@9.0.1: {}
@@ -3195,8 +3896,38 @@ snapshots:
dependencies:
argparse: 2.0.1
+ jsdom@25.0.1:
+ dependencies:
+ cssstyle: 4.6.0
+ data-urls: 5.0.0
+ decimal.js: 10.6.0
+ form-data: 4.0.5
+ html-encoding-sniffer: 4.0.0
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ is-potential-custom-element-name: 1.0.1
+ nwsapi: 2.2.23
+ parse5: 7.3.0
+ rrweb-cssom: 0.7.1
+ saxes: 6.0.0
+ symbol-tree: 3.2.4
+ tough-cookie: 5.1.2
+ w3c-xmlserializer: 5.0.0
+ webidl-conversions: 7.0.0
+ whatwg-encoding: 3.1.1
+ whatwg-mimetype: 4.0.0
+ whatwg-url: 14.2.0
+ ws: 8.21.0
+ xml-name-validator: 5.0.0
+ transitivePeerDependencies:
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+
json-buffer@3.0.1: {}
+ json-logic-js@2.0.5: {}
+
json-schema-traverse@0.4.1: {}
json-schema-traverse@1.0.0: {}
@@ -3209,6 +3940,10 @@ snapshots:
optionalDependencies:
graceful-fs: 4.2.11
+ jstimezonedetect@1.0.7: {}
+
+ jwt-decode@3.1.2: {}
+
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -3228,12 +3963,22 @@ snapshots:
dependencies:
p-locate: 5.0.0
+ lodash-es@4.18.1: {}
+
+ lodash.clonedeep@4.5.0: {}
+
+ lodash.isequal@4.5.0: {}
+
lodash.merge@4.6.2: {}
lodash.startcase@4.4.0: {}
+ lodash@4.18.1: {}
+
loupe@3.2.1: {}
+ lru-cache@10.4.3: {}
+
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -3279,6 +4024,12 @@ snapshots:
dependencies:
brace-expansion: 1.1.13
+ moment-timezone@0.5.48:
+ dependencies:
+ moment: 2.30.1
+
+ moment@2.30.1: {}
+
mri@1.2.0: {}
ms@2.0.0: {}
@@ -3293,6 +4044,12 @@ snapshots:
negotiator@1.0.0: {}
+ node-fetch@2.6.13:
+ dependencies:
+ whatwg-url: 5.0.0
+
+ nwsapi@2.2.23: {}
+
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
@@ -3344,10 +4101,16 @@ snapshots:
dependencies:
quansync: 0.2.11
+ parchment@3.0.0: {}
+
parent-module@1.0.1:
dependencies:
callsites: 3.1.0
+ parse5@7.3.0:
+ dependencies:
+ entities: 6.0.1
+
parseurl@1.3.3: {}
path-exists@4.0.0: {}
@@ -3405,6 +4168,19 @@ snapshots:
queue-microtask@1.2.3: {}
+ quill-delta@5.1.0:
+ dependencies:
+ fast-diff: 1.3.0
+ lodash.clonedeep: 4.5.0
+ lodash.isequal: 4.5.0
+
+ quill@2.0.3:
+ dependencies:
+ eventemitter3: 5.0.4
+ lodash-es: 4.18.1
+ parchment: 3.0.0
+ quill-delta: 5.1.0
+
range-parser@1.2.1: {}
raw-body@2.5.3:
@@ -3479,6 +4255,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ rrweb-cssom@0.7.1: {}
+
+ rrweb-cssom@0.8.0: {}
+
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -3487,6 +4267,10 @@ snapshots:
safer-buffer@2.1.2: {}
+ saxes@6.0.0:
+ dependencies:
+ xmlchars: 2.2.0
+
section-matter@1.0.0:
dependencies:
extend-shallow: 2.0.1
@@ -3586,10 +4370,14 @@ snapshots:
signal-exit@4.1.0: {}
+ signature_pad@4.2.0: {}
+
slash@3.0.0: {}
source-map-js@1.2.1: {}
+ spark-md5@2.0.2: {}
+
spawndamnit@3.0.1:
dependencies:
cross-spawn: 7.0.6
@@ -3603,6 +4391,8 @@ snapshots:
std-env@3.10.0: {}
+ string-hash@1.1.3: {}
+
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@@ -3621,8 +4411,12 @@ snapshots:
dependencies:
has-flag: 4.0.0
+ symbol-tree@3.2.4: {}
+
term-size@2.2.1: {}
+ ticky@1.0.1: {}
+
tinybench@2.9.0: {}
tinyexec@0.3.2: {}
@@ -3638,12 +4432,32 @@ snapshots:
tinyspy@4.0.4: {}
+ tippy.js@6.3.7:
+ dependencies:
+ '@popperjs/core': 2.11.8
+
+ tldts-core@6.1.86: {}
+
+ tldts@6.1.86:
+ dependencies:
+ tldts-core: 6.1.86
+
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
toidentifier@1.0.1: {}
+ tough-cookie@5.1.2:
+ dependencies:
+ tldts: 6.1.86
+
+ tr46@0.0.3: {}
+
+ tr46@5.1.1:
+ dependencies:
+ punycode: 2.3.1
+
ts-api-utils@2.5.0(typescript@5.9.3):
dependencies:
typescript: 5.9.3
@@ -3668,6 +4482,8 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
+ type-func@1.0.3: {}
+
type-is@1.6.18:
dependencies:
media-typer: 0.3.0
@@ -3704,6 +4520,12 @@ snapshots:
utils-merge@1.0.1: {}
+ uuid@9.0.1: {}
+
+ vanilla-picker@2.12.3:
+ dependencies:
+ '@sphinxxxx/color-conversion': 2.2.2
+
vary@1.1.2: {}
vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0):
@@ -3740,7 +4562,7 @@ snapshots:
fsevents: 2.3.3
tsx: 4.21.0
- vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0):
+ vitest@3.2.4(@types/node@22.19.17)(happy-dom@15.11.7)(jsdom@25.0.1)(tsx@4.21.0):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
@@ -3767,6 +4589,8 @@ snapshots:
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.19.17
+ happy-dom: 15.11.7
+ jsdom: 25.0.1
transitivePeerDependencies:
- jiti
- less
@@ -3781,6 +4605,33 @@ snapshots:
- tsx
- yaml
+ w3c-xmlserializer@5.0.0:
+ dependencies:
+ xml-name-validator: 5.0.0
+
+ webidl-conversions@3.0.1: {}
+
+ webidl-conversions@7.0.0: {}
+
+ whatwg-encoding@3.1.1:
+ dependencies:
+ iconv-lite: 0.6.3
+
+ whatwg-mimetype@3.0.0:
+ optional: true
+
+ whatwg-mimetype@4.0.0: {}
+
+ whatwg-url@14.2.0:
+ dependencies:
+ tr46: 5.1.1
+ webidl-conversions: 7.0.0
+
+ whatwg-url@5.0.0:
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+
which@2.0.2:
dependencies:
isexe: 2.0.0
@@ -3794,6 +4645,12 @@ snapshots:
wrappy@1.0.2: {}
+ ws@8.21.0: {}
+
+ xml-name-validator@5.0.0: {}
+
+ xmlchars@2.2.0: {}
+
yocto-queue@0.1.0: {}
zod-to-json-schema@3.25.2(zod@4.3.6):