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'); + }); + + it('Allow extra tags / attributes', () => { + const safe = Utils.sanitize('
Hi
', { + addTags: ['article'], + addAttr: ['data-tag'], + }); + const html = safe.toString(); + expect(html).toContain(' { + expect(dom).toBeTruthy(); + expect(typeof dom.appendTo).toBe('function'); + expect(typeof dom.prependTo).toBe('function'); + expect(typeof dom.removeChildFrom).toBe('function'); + }); + + it('Manipulate the DOM via @formio/core dom helpers', () => { + const host = document.createElement('div'); + host.id = 'formio'; + document.body.appendChild(host); + + const banner = document.createElement('div'); + banner.className = 'banner'; + banner.textContent = 'Saved!'; + dom.prependTo(banner, host); + expect(host.firstElementChild).toBe(banner); + + dom.removeChildFrom(banner, host); + expect(host.contains(banner)).toBe(false); + + document.body.removeChild(host); + }); +}); diff --git a/packages/skill-tests/src/formio-sdk/utils-misc.test.ts b/packages/skill-tests/src/formio-sdk/utils-misc.test.ts new file mode 100644 index 0000000..24e1ec4 --- /dev/null +++ b/packages/skill-tests/src/formio-sdk/utils-misc.test.ts @@ -0,0 +1,112 @@ +// @ts-nocheck — see utils-evaluator.test.ts for rationale. +// Mirrors every example in +// `plugin/skills/formio-sdk/references/utils-misc.md`. + +import { beforeAll, describe, expect, it } from 'vitest'; +import { Utils } from '@formio/js/utils'; +import { I18n, unwind, override } from '@formio/core'; +import { Formio } from '@formio/js'; + +beforeAll(() => { + Formio.setBaseUrl('https://forms.mysite.com'); + Formio.setProjectUrl('https://forms.mysite.com/myproject'); +}); + +describe('utils-misc.md examples', () => { + it('Utils.fastCloneDeep clones nested submission data', () => { + expect(typeof Utils.fastCloneDeep).toBe('function'); + const submission = { data: { firstName: 'Ada' } }; + const draft = Utils.fastCloneDeep(submission) as typeof submission; + expect(draft).not.toBe(submission); + expect(draft.data.firstName).toBe('Ada'); + draft.data.firstName = 'Edited'; + expect(submission.data.firstName).toBe('Ada'); + }); + + it("Formio.getToken({ decode: true }) decodes the SDK's cached JWT", async () => { + // {"sub":"abc","name":"Ada","exp":9999999999} — unsigned test payload. + // setToken always tries to fetch /current to populate the cached user; + // that fetch fails offline, but the token cache mutation runs first. + const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhYmMiLCJuYW1lIjoiQWRhIiwiZXhwIjo5OTk5OTk5OTk5fQ.sig'; + await Formio.setToken(token).catch(() => undefined); + const claims = Formio.getToken({ decode: true }) as Record; + expect(claims).toBeTruthy(); + expect(claims.sub).toBe('abc'); + expect(claims.name).toBe('Ada'); + await Formio.setToken(null).catch(() => undefined); + }); + + it('Utils.convertFormatToMoment translates Angular date tokens', () => { + const momentFormat = Utils.convertFormatToMoment('MM/dd/yyyy h:mm a'); + expect(momentFormat).toBe('MM/DD/YYYY h:mm A'); + }); + + it('Utils.moment + Utils.currentTimezone format the current time', () => { + expect(typeof Utils.moment).toBe('function'); + expect(typeof Utils.currentTimezone).toBe('function'); + const tz = Utils.currentTimezone(); + expect(typeof tz).toBe('string'); + const out = Utils.moment().tz(tz).format('YYYY-MM-DD HH:mm'); + expect(out).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); + }); + + it('I18n switches language at runtime (from @formio/core)', () => { + expect(typeof I18n).toBe('function'); + const i18n = new I18n(); + i18n.setLanguages({ + en: { hello: 'Hello' }, + fr: { hello: 'Bonjour' }, + }); + i18n.changeLanguage('fr'); + expect(i18n.t('hello')).toBe('Bonjour'); + }); + + it('unwind explodes a submission with a datagrid (from @formio/core)', () => { + expect(typeof unwind).toBe('function'); + 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); + expect(Array.isArray(rows)).toBe(true); + expect(rows.length).toBeGreaterThan(0); + }); + + it('override replaces a method on a class prototype (from @formio/core)', () => { + expect(typeof override).toBe('function'); + 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, + }); + expect(new TextFieldStub().getValue()).toBe('raw'); + }); +}); diff --git a/packages/skill-tests/src/setup.ts b/packages/skill-tests/src/setup.ts new file mode 100644 index 0000000..924b11d --- /dev/null +++ b/packages/skill-tests/src/setup.ts @@ -0,0 +1,9 @@ +// Vitest setup. Runs before every test file. +// +// `@formio/js` pulls in DOM-dependent modules (dragula, DOMPurify, etc.) at +// import time, so `vitest.config.ts` picks `happy-dom` as the test +// environment. Nothing else needs configuring here — the file exists so we +// have a stable place to hang future cross-cutting setup (e.g. fetch mocks, +// localStorage resets). + +export {}; diff --git a/packages/skill-tests/tsconfig.json b/packages/skill-tests/tsconfig.json new file mode 100644 index 0000000..b8e45e7 --- /dev/null +++ b/packages/skill-tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src", "vitest.config.ts"], + "exclude": ["node_modules"] +} diff --git a/packages/skill-tests/vitest.config.ts b/packages/skill-tests/vitest.config.ts new file mode 100644 index 0000000..40f3149 --- /dev/null +++ b/packages/skill-tests/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'jsdom', + globals: true, + include: ['src/**/*.test.ts'], + setupFiles: ['./src/setup.ts'], + testTimeout: 20000, + }, +}); diff --git a/plugin/skills/formio-api/references/platform-auth.md b/plugin/skills/formio-api/references/platform-auth.md index 15ef444..85fe58d 100644 --- a/plugin/skills/formio-api/references/platform-auth.md +++ b/plugin/skills/formio-api/references/platform-auth.md @@ -68,14 +68,14 @@ Errors: `400` on duplicate email or missing fields; `401`/`403` if the caller la Same endpoint as the admin login above, but invoked with a regular platform user's credentials. Form.io does not distinguish admin vs user at the endpoint level — role membership in the response body determines capability. -### GET ${FORMIO_BASE_URL}/formio/current +### GET ${FORMIO_BASE_URL}/current Return the submission document for the currently authenticated platform user. Response: the user's submission document (same shape as login response, minus JWT issuance). `401` if the `x-jwt-token` header is missing or expired. ```bash -curl -H "x-jwt-token: $FORMIO_JWT" "${FORMIO_BASE_URL}/formio/current" +curl -H "x-jwt-token: $FORMIO_JWT" "${FORMIO_BASE_URL}/current" ``` ### GET ${FORMIO_BASE_URL}/formio/logout diff --git a/plugin/skills/formio-api/references/runtime-auth.md b/plugin/skills/formio-api/references/runtime-auth.md index 975da47..96bbfe8 100644 --- a/plugin/skills/formio-api/references/runtime-auth.md +++ b/plugin/skills/formio-api/references/runtime-auth.md @@ -77,7 +77,7 @@ curl -X POST -H "Content-Type: application/json" \ "${FORMIO_PROJECT_URL}/user/login/submission" ``` -### GET ${FORMIO_PROJECT_URL}/current +### GET ${FORMIO_BASE_URL}/current Return the currently authenticated user's submission document. Used to rehydrate user state on app start or to check that a stored JWT is still valid. @@ -89,7 +89,7 @@ Example: ```bash curl -H "x-jwt-token: $FORMIO_JWT" \ - "${FORMIO_PROJECT_URL}/current" + "${FORMIO_BASE_URL}/current" ``` ### GET ${FORMIO_PROJECT_URL}/logout @@ -107,7 +107,7 @@ curl -H "x-jwt-token: $FORMIO_JWT" \ "${FORMIO_PROJECT_URL}/logout" ``` -### GET ${FORMIO_PROJECT_URL}/current (session expired) +### GET ${FORMIO_BASE_URL}/current (session expired) Same endpoint as `Get Current User`, but documents the expired-session behavior. When the supplied `x-jwt-token` has expired or been revoked (e.g., after calling `logout`), Form.io responds with a plain-text body: @@ -121,7 +121,7 @@ Example: ```bash curl -H "x-jwt-token: $EXPIRED_JWT" \ - "${FORMIO_PROJECT_URL}/current" + "${FORMIO_BASE_URL}/current" ``` ## Related Skills diff --git a/plugin/skills/formio-sdk/SKILL.md b/plugin/skills/formio-sdk/SKILL.md new file mode 100644 index 0000000..1dffffb --- /dev/null +++ b/plugin/skills/formio-sdk/SKILL.md @@ -0,0 +1,111 @@ +--- +name: formio-sdk +description: >- + Reference for the Form.io JavaScript SDK (`@formio/js`), the Form.io Utilities (`@formio/js/utils`), and the small set of helpers exposed only by `@formio/core` (jsonLogic, dom, I18n, override, unwind, the logic processor). Covers static SDK methods (`setBaseUrl`, `setProjectUrl`, `setToken`, `currentUser`, `logout`, `ssoInit`, `accessInfo`, `pluginAlter`, `pluginGet`, `pluginWait`), instance SDK methods on `new Formio(url)` (`loadForm`, `saveForm`, `deleteForm`, `loadForms`, `loadSubmission`, `saveSubmission`, `deleteSubmission`, `loadSubmissions`, `availableActions`, `userPermissions`, `getDownloadUrl`, `getTempToken`, `uploadFile`, `downloadFile`, `deleteFile`), VanillaJS form rendering (`Formio.createForm`, `Formio.builder`, `form.on(…)`, `form.submission` prefill), the plugin lifecycle (`preRequest`, `request`, `staticRequest`, `wrapRequestPromise`, `wrapStaticRequestPromise`, `requestOptions`, `requestResponse`), the `Utils` surface (Evaluator, form traversal, conditions, mask, sanitize, date helpers, fastCloneDeep), and the @formio/core-only surfaces (`jsonLogic`, `dom`, `I18n`, `override`, `unwind`, `logicProcessSync`/`logicProcessInfo`). Token decoding goes through `Formio.getToken({ decode: true })`; logout and token clearing through `Formio.logout()` or `Formio.setToken(null)`. Use when the user asks to call any `Formio.*` static method, work with a `new Formio(...)` instance, render a Form.io form in a VanillaJS or non-Angular consumer, invoke a `Utils.*` helper, evaluate JSONLogic, run component-logic actions, manipulate the DOM via Form.io's helpers, register a plugin, configure `setBaseUrl`/`setProjectUrl` for a Hosted or SaaS deployment, traverse component trees, mask or sanitize input, or decode the cached JWT. Not for: documenting Form.io REST endpoint shape (see formio-api); orchestrating a full application build (see formio-application); planning Resource schemas (see formio-resource-planner); Angular `@formio/angular` wrappers, `FormioAppConfig`, or `FormioModule` (see formio-angular). +--- + +# Form.io SDK Skills + +Reference for `@formio/js`, `@formio/js/utils`, and the helpers exposed only by `@formio/core`. Covers SDK bootstrap, authentication, form / submission / project / role / file CRUD, plugin lifecycle, VanillaJS rendering, and the full `Utils` surface (Evaluator, traversal, conditions, logic actions, JSONLogic, mask, sanitize, date, DOM, i18n, fastCloneDeep, override, unwind). + +## Imports + +Prefer the renderer-extended SDK first; fall back to `@formio/core` only when a surface is not re-exported by `@formio/js` or `@formio/js/utils`: + +```ts +// Preferred — covers the SDK, rendering, plugins, forms, submissions, projects, +// roles, files, and the bulk of the Utils surface. +import { Formio } from '@formio/js'; +import { Utils } from '@formio/js/utils'; + +// Acceptable fallbacks — only when @formio/js does not expose the surface. +// Confirmed-needed today: jsonLogic, dom, I18n, override, unwind, the +// runtime logic processor (logicProcessSync), and the canonical DefaultEvaluator +// base class. +import { jsonLogic, dom, I18n, override, unwind, sanitize } from '@formio/core'; +import { logicProcessSync, logicProcessInfo } from '@formio/core/process'; +``` + +Never use `@formio/js/lib/...` deep imports or `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):