diff --git a/.gitignore b/.gitignore index bb140a3c..f03ba3ac 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,13 @@ dist/ bun.lockb .publish/ + +# Environment files may contain secrets. Commit only templates. +.env +.env.* +!.env.example +!.env.*.example + +# Local focused typecheck helpers must never enter the repository. +focus-shims.d.ts +tsconfig.focus.json diff --git a/.prettierignore b/.prettierignore index b38548d9..e6a6fd4d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,3 +15,9 @@ bun.lockb editors/vscode/src/compiler.cjs *.svg **/.vscodeignore + +# Local focused typecheck helpers (never part of a release) +focus-shims.d.ts +**/focus-shims.d.ts +tsconfig.focus.json +**/tsconfig.focus.json diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 03238614..bb84f519 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 32c23792..71609734 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 3d58a853..6ca2aa1b 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,18 +28,18 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.6.0", - "@wrnexus/router": "^0.6.0", - "@wrnexus/csr": "^0.6.0", - "@wrnexus/compiler": "^0.6.0", - "@wrnexus/styles": "^0.6.0", - "@wrnexus/dev-server": "^0.6.0", - "@wrnexus/ui": "^0.6.0", - "@wrnexus/validation": "^0.6.0", - "@wrnexus/i18n": "^0.6.0", - "@wrnexus/db": "^0.6.0", - "@wrnexus/plugin": "^0.6.0", - "@wrnexus/syntax": "^0.6.0" + "@wrnexus/core": "^0.7.0", + "@wrnexus/router": "^0.7.0", + "@wrnexus/csr": "^0.7.0", + "@wrnexus/compiler": "^0.7.0", + "@wrnexus/styles": "^0.7.0", + "@wrnexus/dev-server": "^0.7.0", + "@wrnexus/ui": "^0.7.0", + "@wrnexus/validation": "^0.7.0", + "@wrnexus/i18n": "^0.7.0", + "@wrnexus/db": "^0.7.0", + "@wrnexus/plugin": "^0.7.0", + "@wrnexus/syntax": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 4addc0c7..362f4be3 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/syntax": "^0.6.0", - "@wrnexus/store": "^0.6.0" + "@wrnexus/syntax": "^0.7.0", + "@wrnexus/store": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/core/package.json b/.publish/core/package.json index ea549dc7..b65f36b9 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 8713b037..47d4498c 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.6.0" + "@wrnexus/core": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index ea554d7d..bbd44a41 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 8a0ca71b..87ebf0b1 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,21 +25,23 @@ } }, "dependencies": { - "@wrnexus/core": "^0.6.0", - "@wrnexus/dev-toolbar": "^0.6.0", - "@wrnexus/router": "^0.6.0", - "@wrnexus/ssr": "^0.6.0", - "@wrnexus/csr": "^0.6.0", - "@wrnexus/compiler": "^0.6.0", - "@wrnexus/styles": "^0.6.0", - "@wrnexus/ui": "^0.6.0", - "@wrnexus/validation": "^0.6.0", - "@wrnexus/i18n": "^0.6.0", - "@wrnexus/db": "^0.6.0", - "@wrnexus/pubsub": "^0.6.0", - "@wrnexus/uploader": "^0.6.0", - "@wrnexus/plugin": "^0.6.0", - "@wrnexus/store": "^0.6.0" + "@wrnexus/core": "^0.7.0", + "@wrnexus/dev-toolbar": "^0.7.0", + "@wrnexus/router": "^0.7.0", + "@wrnexus/ssr": "^0.7.0", + "@wrnexus/csr": "^0.7.0", + "@wrnexus/compiler": "^0.7.0", + "@wrnexus/styles": "^0.7.0", + "@wrnexus/ui": "^0.7.0", + "@wrnexus/validation": "^0.7.0", + "@wrnexus/i18n": "^0.7.0", + "@wrnexus/db": "^0.7.0", + "@wrnexus/pubsub": "^0.7.0", + "@wrnexus/uploader": "^0.7.0", + "@wrnexus/plugin": "^0.7.0", + "@wrnexus/store": "^0.7.0", + "@wrnexus/security": "^0.7.0", + "@wrnexus/observability": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/dev-toolbar/package.json b/.publish/dev-toolbar/package.json index 418c0b3f..2a9f5e30 100644 --- a/.publish/dev-toolbar/package.json +++ b/.publish/dev-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-toolbar", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 27ebad40..895cedb4 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index e34f0b7d..46c0349f 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.6.0" + "@wrnexus/core": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 114a30e5..fd740463 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.6.0" + "@wrnexus/core": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 3c1e592b..4655b47f 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index e1d2c669..89b6e9d5 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.6.0" + "@wrnexus/native": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index 11c0b3c1..b9ac5cb0 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 17cc0912..9046302d 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/plugin/package.json b/.publish/plugin/package.json index 5ab4b0ca..599aefbe 100644 --- a/.publish/plugin/package.json +++ b/.publish/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/plugin", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/plugin — part of the WrNexus framework.", "license": "MIT", @@ -33,7 +33,7 @@ } }, "dependencies": { - "@wrnexus/syntax": "^0.6.0" + "@wrnexus/syntax": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index 712fa7a6..b1cd80b8 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index dceaeb87..1bc6b908 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index b2504c33..d71e8783 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 1b8ee612..c9894fa3 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.6.0", - "@wrnexus/core": "^0.6.0" + "@wrnexus/compiler": "^0.7.0", + "@wrnexus/core": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index c982c104..2c4e5926 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -29,8 +29,9 @@ } }, "dependencies": { - "@wrnexus/core": "^0.6.0", - "@wrnexus/store": "^0.6.0" + "@wrnexus/core": "^0.7.0", + "@wrnexus/store": "^0.7.0", + "@wrnexus/security": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 212c0667..8530ef84 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,9 +21,9 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.6.0", - "@wrnexus/core": "^0.6.0", - "@wrnexus/plugin": "^0.6.0" + "@wrnexus/uploader": "^0.7.0", + "@wrnexus/core": "^0.7.0", + "@wrnexus/plugin": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/syntax/package.json b/.publish/syntax/package.json index 18c92779..a82bc899 100644 --- a/.publish/syntax/package.json +++ b/.publish/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/syntax — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/test/package.json b/.publish/test/package.json index 6ef1dfd2..5b3cf7b9 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 68a5e664..bc4741bd 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 895b1c57..6a820e48 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -26,7 +26,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.6.0" + "@wrnexus/core": "^0.7.0" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index 1277713d..42421953 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.6.0" + "@wrnexus/core": "^0.7.0" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 29fe3360..cb4e1bab 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/CHANGES-0.6.0.md b/CHANGES-0.6.0.md index 0a718b38..ec1a8fee 100644 --- a/CHANGES-0.6.0.md +++ b/CHANGES-0.6.0.md @@ -404,3 +404,18 @@ Comparison base: `WRNexusJS(4).zip`. Dependency directories, `.git`, and `.wrnex - Prevented the UI reference generator from rewriting current files solely because of CRLF/LF differences. - Added an executable regression test proving line-ending-only differences are ignored and real stale content is repaired. + +## 0.7.0 R2 typecheck correction + +- Removed leaked focused-typecheck shim files that polluted the root TypeScript program. +- Added a validation gate preventing those temporary files from entering future archives. + +## 0.7.0 R4 Happy DOM event typing + +- Fixed strict event-type compatibility in `packages/validation/test/validation.test.ts`. +- Improved `check:workspace` dry-run wording. +- Added a validation guard preventing browser DOM `Event` from being reintroduced as the Happy DOM helper return type. + +## 0.7.0 R6 lint cleanup + +See `FIXES-0.7.0-R6-LINT-CLEANUP.md` for the final lint and secure hydration corrections. diff --git a/FIXES-0.7.0-R1-LOCAL-ENV-AUDIT.md b/FIXES-0.7.0-R1-LOCAL-ENV-AUDIT.md new file mode 100644 index 00000000..d5b10c96 --- /dev/null +++ b/FIXES-0.7.0-R1-LOCAL-ENV-AUDIT.md @@ -0,0 +1,30 @@ +# WRNexusJS 0.7.0 R1 — Local environment audit boundary + +## Problem + +The security audit recursively scanned every file in the working directory. Local, ignored environment files such as `examples/basic-app/.env` and `.env.uat` therefore failed the release audit even though they were not tracked by Git and would not be published. + +## Fix + +- Release security checks now scan `git ls-files -z` when Git metadata is available. +- Secret-like files tracked by Git still fail with `SEC-NO-TRACKED-SECRET-FILES`. +- Ignored or untracked local secret files produce `SEC-LOCAL-SECRET-FILES` warnings only. +- Source archives without `.git` metadata still scan every included file. +- The JSON report schema is updated to version 2 and includes warnings separately from errors. + +## Existing repositories + +If old `.env` files are still tracked, remove them from the Git index while retaining local copies: + +```powershell +git rm --cached examples/basic-app/.env examples/basic-app/.env.uat +git add examples/basic-app/.env.example examples/basic-app/.env.uat.example .gitignore +git commit -m "security(examples): stop tracking local environment files" +``` + +The root `.gitignore` now also explicitly allows environment templates for named environments: + +```gitignore +!.env.example +!.env.*.example +``` diff --git a/FIXES-0.7.0-R2-TYPECHECK-SHIM-LEAK.md b/FIXES-0.7.0-R2-TYPECHECK-SHIM-LEAK.md new file mode 100644 index 00000000..a3e382d1 --- /dev/null +++ b/FIXES-0.7.0-R2-TYPECHECK-SHIM-LEAK.md @@ -0,0 +1,19 @@ +# WRNexusJS 0.7.0 R2 — Root Typecheck Fix + +## Cause + +The R1 source archive accidentally contained two temporary files used only for a dependency-free focused compile: + +- `focus-shims.d.ts` +- `tsconfig.focus.json` + +The root `tsc --noEmit` command automatically included `focus-shims.d.ts`. Its intentionally broad declarations replaced or merged with the installed Bun, Node.js, TypeScript, Happy DOM, filesystem, child-process, and build-tool declarations. That caused callback parameters to become implicit `any`, removed real Bun APIs and generics, and produced the reported 78 errors across otherwise valid source files. + +## Fix + +- Removed `focus-shims.d.ts` from the source tree. +- Removed `tsconfig.focus.json` from the source tree. +- Added a `validate:0.7` release guard that fails if either temporary file is present. +- Re-ran the root TypeScript program with typed Bun/Node/TypeScript module declarations. + +No public framework API or runtime behavior changed in this correction. diff --git a/FIXES-0.7.0-R3-WORKSPACE-REPAIR.md b/FIXES-0.7.0-R3-WORKSPACE-REPAIR.md new file mode 100644 index 00000000..e4a5b99e --- /dev/null +++ b/FIXES-0.7.0-R3-WORKSPACE-REPAIR.md @@ -0,0 +1,43 @@ +# WRNexusJS 0.7.0 R3 — Workspace Repair and Typecheck Isolation + +## Problem + +Applying a source ZIP over an existing Git checkout cannot delete files or change the Git index. Two local focused-typecheck files and two previously tracked environment files therefore survived earlier archive updates: + +- `focus-shims.d.ts` +- `tsconfig.focus.json` +- `examples/basic-app/.env` +- `examples/basic-app/.env.uat` + +The ambient declarations in `focus-shims.d.ts` replaced or weakened the installed Bun, Node.js, TypeScript, filesystem, child-process, Happy DOM, and tsup typings. That produced dozens of false `implicit any`, missing namespace member, and untyped `Bun.serve` errors. + +## Corrections + +1. Root `tsconfig.json` now has explicit include/exclude rules and always excludes focused-typecheck helper files. +2. Added `bun run repair:workspace` to: + - remove the two temporary typecheck files; + - remove unsafe secret-like files and temporary shims from Git tracking; + - preserve local `.env` file contents; + - ensure the required `.gitignore` rules exist. +3. Added `bun run check:workspace` for non-mutating CI/release verification. +4. `validate:0.7` warns about local leftovers instead of allowing a TypeScript cascade, and verifies that the root typecheck excludes them. +5. The security audit blocks tracked typecheck shims and tracked environment files with the repair command in its error message. +6. Release prepare/publish now runs `check:workspace` before validation. +7. Package staging rejects `focus-shims.d.ts` and `tsconfig.focus.json`. +8. Added an executable Git regression test proving local environment values are retained while unsafe files are removed from tracking. + +## Required one-time command for an existing checkout + +```powershell +bun run repair:workspace +``` + +Then review and commit the index changes: + +```powershell +git status +git add .gitignore package.json tsconfig.json scripts + +git commit -m "fix(tooling): isolate temporary typecheck files and repair workspace state" +git push origin main +``` diff --git a/FIXES-0.7.0-R4-HAPPY-DOM-EVENT-TYPES.md b/FIXES-0.7.0-R4-HAPPY-DOM-EVENT-TYPES.md new file mode 100644 index 00000000..94088d44 --- /dev/null +++ b/FIXES-0.7.0-R4-HAPPY-DOM-EVENT-TYPES.md @@ -0,0 +1,29 @@ +# WRNexusJS 0.7.0 R4 — Happy DOM Event Type Correction + +## Problem + +`packages/validation/test/validation.test.ts` returned the browser DOM `Event` type from its synthetic event helper. Happy DOM elements require Happy DOM's own event class, so strict TypeScript reported two incompatible `dispatchEvent()` calls. + +## Fix + +The helper now derives its event type directly from Happy DOM's own `document.dispatchEvent` parameter: + +```ts +type HappyDOMEvent = Parameters[0]; +type HappyDOMEventConstructor = new (type: string, init?: EventInit) => HappyDOMEvent; + +function windowEvent(win: Window, type: string, init?: EventInit): HappyDOMEvent { + const EventConstructor = (win as unknown as { Event: HappyDOMEventConstructor }).Event; + return new EventConstructor(type, init); +} +``` + +This keeps browser DOM tests and Happy DOM tests type-safe without using an unsafe browser `Event` return type. + +## Workspace repair + +The two `.env` files and temporary focused-typecheck files shown by `check:workspace` are local leftovers. Run `bun run repair:workspace` once before the security audit. The command preserves local `.env` contents while removing unsafe files from Git tracking. + +## Additional improvement + +`check:workspace` now says `would remove` in check mode, instead of incorrectly implying files were already removed. diff --git a/FIXES-0.7.0-R5-HAPPY-DOM-ELEMENT-EVENT-TYPES.md b/FIXES-0.7.0-R5-HAPPY-DOM-ELEMENT-EVENT-TYPES.md new file mode 100644 index 00000000..761d30d1 --- /dev/null +++ b/FIXES-0.7.0-R5-HAPPY-DOM-ELEMENT-EVENT-TYPES.md @@ -0,0 +1,21 @@ +# WRNexusJS 0.7.0 R5 — Happy DOM element/event type alignment + +## Problem + +R4 returned Happy DOM `Event` objects from the test helper, but two inputs were still cast to the browser DOM `HTMLInputElement` type. TypeScript therefore rejected the Happy DOM event at `dispatchEvent()`. + +## Correction + +`packages/validation/test/validation.test.ts` now imports and consistently uses Happy DOM types for: + +- `Event` +- `IEventInit` +- `HTMLElement` +- `HTMLInputElement` +- `HTMLFormElement` + +The input, field, error and form nodes no longer use browser DOM global casts. Submit events also no longer cast back to the browser DOM `Event` type. + +## Regression gate + +`validate:0.7` now verifies that the validation tests use one Happy DOM type system and rejects the old `as unknown as HTMLInputElement`, `HTMLFormElement`, or `HTMLElement` casts. diff --git a/FIXES-0.7.0-R6-LINT-CLEANUP.md b/FIXES-0.7.0-R6-LINT-CLEANUP.md new file mode 100644 index 00000000..16ab416f --- /dev/null +++ b/FIXES-0.7.0-R6-LINT-CLEANUP.md @@ -0,0 +1,26 @@ +# WRNexusJS 0.7.0 R6 - Lint cleanup + +R6 fixes the remaining root ESLint findings reported after R5. + +## Corrections + +- Replaced control-character regular expressions in compiler, security URL validation, and syntax diagnostics with explicit ASCII character-code checks. +- `renderStoreHydration()` now actually uses `serializeForHtml()` for bounded, prototype-safe, HTML-safe hydration JSON. +- SBOM parse errors preserve the original caught error through `ErrorOptions.cause`. +- The workspace repair regression imports `node:process` explicitly. +- ESLint and Prettier ignore local `focus-shims.d.ts` and `tsconfig.focus.json` leftovers until `bun run repair:workspace` removes them. +- Added `validate:0.7` guards for every R6 regression. + +## Security behavior retained + +URLs containing ASCII whitespace, C0 control characters, or DEL are rejected by `@wrnexus/security`. Static compiler and syntax checks continue to normalize embedded C0 characters before detecting dangerous protocols such as `javascript:`. + +## Local validation + +Run on the target Bun workspace: + +```powershell +bun run repair:workspace +bun install +bun run check +``` diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0-R1.json b/FOCUSED-VALIDATION-RESULTS-0.7.0-R1.json new file mode 100644 index 00000000..dfc4c2dd --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0-R1.json @@ -0,0 +1,20 @@ +{ + "release": "0.7.0-r1", + "fix": "Git-tracked secret-file audit boundary", + "results": { + "validate_0_7": { + "passed": 35, + "failed": 0 + }, + "clean_archive_security_audit": "passed", + "ignored_local_env_files": { + "result": "warning-only", + "exitCode": 0 + }, + "git_tracked_env_files": { + "result": "blocked", + "exitCode": 1 + }, + "javascript_syntax": "passed" + } +} diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0-R2.json b/FOCUSED-VALIDATION-RESULTS-0.7.0-R2.json new file mode 100644 index 00000000..ee77f0f2 --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0-R2.json @@ -0,0 +1,30 @@ +{ + "release": "0.7.0-r2", + "reportedTypeScriptErrors": 78, + "reportedFiles": 28, + "rootCause": [ + "focus-shims.d.ts was accidentally included in the release source and automatically loaded by the root TypeScript program", + "tsconfig.focus.json was a temporary packaging-only configuration and should not have been distributed" + ], + "removedFiles": ["focus-shims.d.ts", "tsconfig.focus.json"], + "hardening": [ + "validate:0.7 now rejects temporary focused-typecheck files", + "compressed response bodies are copied into a concrete ArrayBuffer for TypeScript 5.9 DOM compatibility", + "Happy DOM event constructors are accessed through explicit typed window shapes", + "Redis tests use the typed Bun global directly", + "package publishing imports node:process explicitly" + ], + "validation": { + "rootTypeScriptProgram": "passed", + "validate07": { "passed": 36, "failed": 0 }, + "securityFramework": { "passed": 10, "failed": 0 }, + "validate06Compatibility": { "passed": 20, "warnings": 1, "failed": 0 }, + "vscodeTests": { "passed": 28, "failed": 0 }, + "vscodeValidation": "passed", + "temporaryFileGuard": { + "temporaryFilePresentExitCode": 1, + "temporaryFileAbsentExitCode": 0 + } + }, + "localAuthoritativeCommand": "bun run check" +} diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0-R3.json b/FOCUSED-VALIDATION-RESULTS-0.7.0-R3.json new file mode 100644 index 00000000..0af18b20 --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0-R3.json @@ -0,0 +1,28 @@ +{ + "release": "0.7.0-r3", + "focus": "workspace repair, tracked secret cleanup, and root typecheck isolation", + "results": { + "validate_0_7": { + "passed": 37, + "warnings": 0, + "failed": 0 + }, + "security_framework": "passed", + "validate_0_6_compatibility": { + "passed": 20, + "warnings": 1, + "failed": 0 + }, + "workspace_repair_git_regression": "passed", + "package_staging_integrity": "passed", + "temporary_shim_excluded_from_root_typescript_program": "passed", + "vscode_tests": { + "passed": 28, + "failed": 0 + }, + "vscode_extension_validation": "passed", + "sbom_components": 339 + }, + "local_full_bun_check_required": true, + "required_command": "bun run repair:workspace && bun run check" +} diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0-R4.json b/FOCUSED-VALIDATION-RESULTS-0.7.0-R4.json new file mode 100644 index 00000000..0a6ebdde --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0-R4.json @@ -0,0 +1,26 @@ +{ + "release": "0.7.0-r4", + "fix": "Happy DOM synthetic event type compatibility", + "results": { + "validate_0_7": { + "passed": 38, + "warnings": 0, + "failed": 0 + }, + "security_framework_audit": "passed", + "workspace_repair_regression": "passed", + "validate_0_6_compatibility": { + "passed": 20, + "warnings": 1, + "failed": 0 + }, + "vscode_tests": { + "passed": 28, + "failed": 0 + }, + "vscode_extension_validation": "passed", + "happy_dom_event_type_structural_compile": "passed", + "zip_integrity": "passed" + }, + "environment_limit": "Bun and the repository dependency tree are unavailable in the packaging environment, so the complete bun run check remains required on the target Windows workspace." +} diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0-R5.json b/FOCUSED-VALIDATION-RESULTS-0.7.0-R5.json new file mode 100644 index 00000000..88ffdf25 --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0-R5.json @@ -0,0 +1,31 @@ +{ + "release": "WRNexusJS 0.7.0 R5", + "fix": "Happy DOM event and element receiver type alignment", + "validation": { + "validate_0_7": { + "passed": 38, + "warnings": 0, + "failed": 0 + }, + "security_framework": "passed", + "validate_0_6": { + "passed": 20, + "warnings": 1, + "failed": 0 + }, + "vscode_tests": { + "passed": 28, + "failed": 0 + }, + "vscode_extension_validation": "passed", + "focused_happy_dom_type_compile": "passed", + "full_bun_typecheck": "requires target Windows environment" + }, + "changed_files": [ + "packages/validation/test/validation.test.ts", + "scripts/validate-0.7.mjs", + "FIXES-0.7.0-R5-HAPPY-DOM-ELEMENT-EVENT-TYPES.md", + "CHANGES-0.6.0.md", + "RELEASE_NOTES-0.7.0.md" + ] +} diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0-R6.json b/FOCUSED-VALIDATION-RESULTS-0.7.0-R6.json new file mode 100644 index 00000000..38648cc4 --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0-R6.json @@ -0,0 +1,19 @@ +{ + "release": "0.7.0-r6", + "reportedLintFindings": 8, + "reportedErrorsFixed": 5, + "reportedWarningsFixedOrIsolated": 3, + "validation": { + "validate07": { "passed": 41, "warnings": 0, "failed": 0 }, + "securityFrameworkAudit": "passed", + "workspaceRepairRegression": "passed", + "packageStagingIntegrity": "passed", + "focusedTypeScript": "passed", + "urlControlCharacterProbe": "passed", + "compatibilityValidator06": { "passed": 20, "warnings": 1, "failed": 0 }, + "vscodeTests": { "passed": 28, "failed": 0 }, + "vscodeValidation": "passed", + "benchmarkFramework": "passed" + }, + "environmentBoundary": "Bun and repository ESLint dependencies are unavailable in the packaging environment; run bun run check on the target Windows workspace." +} diff --git a/FOCUSED-VALIDATION-RESULTS-0.7.0.json b/FOCUSED-VALIDATION-RESULTS-0.7.0.json new file mode 100644 index 00000000..b2903045 --- /dev/null +++ b/FOCUSED-VALIDATION-RESULTS-0.7.0.json @@ -0,0 +1,68 @@ +{ + "release": "0.7.0", + "implementation": { + "requestedAreas": 21, + "implementedAreas": 21, + "newPackages": 5, + "frameworkPackages": 38 + }, + "validators": { + "v070": { + "passed": 34, + "failed": 0 + }, + "v060Compatibility": { + "passed": 20, + "warnings": 1, + "failed": 0, + "warning": "Bun unavailable in packaging environment" + }, + "securityPerformanceAudit": { + "passed": 10, + "failed": 0 + } + }, + "compiler": { + "wrnFilesCompiled": 281, + "uiComponentsCompiled": 108, + "browserModulesSyntaxChecked": 108, + "failures": 0 + }, + "vscode": { + "compilerModulesBundled": 24, + "testsPassed": 28, + "testsFailed": 0, + "extensionValidationPassed": true + }, + "supplyChain": { + "sbomFormat": "CycloneDX 1.5", + "sbomComponents": 339, + "workspaceComponents": 38, + "deterministic": true, + "packageStageIntegrityProbePassed": true + }, + "benchmarks": { + "syntaxParseComponent": { + "meanMs": 0.19686705000000188, + "p95Ms": 0.4446000000000083, + "operationsPerSecond": 5079.570197247282 + }, + "secureHydrationSerialization": { + "meanMs": 0.05032330799999943, + "p95Ms": 0.07434000000000651, + "operationsPerSecond": 19871.50765208065 + }, + "tagCacheRead100": { + "meanMs": 0.018023848000000498, + "p95Ms": 0.04413500000001136, + "operationsPerSecond": 55482.04800661725 + } + }, + "typeScript": { + "focusedStrictIntegrationPassed": true + }, + "completeBunCheck": { + "runInPackagingEnvironment": false, + "requiredOnTargetMachine": true + } +} diff --git a/IMPLEMENTATION-0.7.0-21-POINTS.md b/IMPLEMENTATION-0.7.0-21-POINTS.md new file mode 100644 index 00000000..e4ddbc71 --- /dev/null +++ b/IMPLEMENTATION-0.7.0-21-POINTS.md @@ -0,0 +1,67 @@ +# WRNexusJS 0.7.0 — 21-Point Security and Performance Implementation + +WRNexusJS 0.7.0 implements the framework-side foundations for all 21 requested security and performance areas. Secure defaults, compiler diagnostics, runtime enforcement, production reports, release gates, and adapter interfaces are included in this source release. + +External infrastructure remains an application/deployment responsibility: TLS termination, Redis or another distributed cache, antivirus/CDR engines, image transcoding services, CDN configuration, an OTLP/metrics backend, database indexes, registry MFA, and project-specific authorization policy. + +## Implementation matrix + +| # | Area | Implemented in 0.7.0 | +| --: | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | Targets and budgets | Expanded route, CSS, HTML, image, hydration, SSR, Web Vitals, request-count, and long-task budgets with warning/error enforcement and build reporting. | +| 2 | Compiler/template security | Context-aware URL validation, unsafe protocol diagnostics, dangerous DOM/dynamic-code diagnostics, client/server boundary checks, persisted-secret checks, Trusted HTML policy, and bounded secure hydration serialization. | +| 3 | Browser headers | CSP with nonce/report-only support, Trusted Types, HSTS, COOP, CORP, Referrer-Policy, Permissions-Policy, nosniff, frame protection, Origin-Agent-Cluster, and presets. | +| 4 | Sessions/authentication | Idle and absolute expiry, sliding access, secure cookie defaults, server-side session backend support, and safer session lifecycle configuration. | +| 5 | CSRF/CORS/authorization | CSRF token plus Origin and Fetch Metadata validation, opt-in exact-origin CORS, and continued server-side authz/tenant policy enforcement. | +| 6 | Gateway/API hardening | URL/header/query/body limits, timeouts, concurrency limits, host/proxy trust, request IDs, IP rate limits, Fetch Metadata, access logs, and WebSocket queue/origin/message controls. | +| 7 | SSRF | Safe fetch validates protocols and hosts, resolves and checks addresses, blocks private/special networks, revalidates redirects, limits size/time/redirects, and avoids credential forwarding. | +| 8 | Uploads | Extension/MIME/size/aggregate/path controls, generated names, secure download disposition, inspector hooks, and antivirus/CDR adapter interfaces. | +| 9 | Database | Query timing, timeouts, row caps, slow-query reporting, duplicate/N+1 detection, SELECT-star/unbounded-query warnings, and per-request query records. | +| 10 | SSR/server | Runtime route classification, zero-JS/prerender analysis, request dedupe and cache foundations, streaming compatibility, and build-report visibility. | +| 11 | Hydration/CSR | Static pages default to zero framework JavaScript, auto/client/document navigation modes, partial/lazy hydration foundations, batched reactive work, keyed updates, cancellation/disposal, and hydration telemetry. | +| 12 | Build/bundles | Server/client separation, minification/hashing support, immutable runtime assets, runtime classification, production build reports, and enforceable budgets. | +| 13 | CSS | Existing minification/token tooling plus diagnostics for transition-all, expensive blur/shadow patterns, broad selectors, and duplicate keyframes. | +| 14 | Images/icons/fonts | Responsive image planning, srcset/sizes/dimensions/loading/decoding/fetch-priority policy, remote-host validation, LCP/oversizing audits, and continued per-icon/font optimization helpers. | +| 15 | HTTP delivery | Brotli-first negotiation, gzip fallback, compression exclusions, ETags/conditional responses, immutable assets, cache-control helpers, stale policies, and preload/modulepreload support. | +| 16 | Realtime/WebSocket | Authentication, origins, message byte/rate/depth limits, room/user quotas, schema and authorization predicates, prototype-pollution rejection, and gateway backpressure controls. | +| 17 | Observability | Counters, gauges, histograms, HTTP middleware, request/error/active-request metrics, same-origin Web Vitals, browser collector, exporters, and Server-Timing integration. | +| 18 | DevToolbar | Security checks for forms/CSRF/CSP/mixed content/storage/hydration leaks and performance checks for transfer, DOM, hydration, images, blocking assets, and long tasks. | +| 19 | Tests/release gates | 0.7 validator, production security audit, benchmark budgets, type/lint/test/format pipeline, deterministic UI references, stage integrity checks, and compatibility validation. | +| 20 | Supply chain | Private release policy, aligned versions, explicit migration, deterministic CycloneDX SBOM, secret/source-map/source leakage scanning, package SHA-256 manifests, and clean/pushed Git requirements. | +| 21 | New packages | `@wrnexus/security`, `@wrnexus/cache`, `@wrnexus/image`, `@wrnexus/observability`, and `@wrnexus/benchmark`. | + +## Compatibility + +- Existing 0.6 component, parser, store, RPC, and runtime contracts remain supported. +- The updater preserves existing security, performance, observability, and navigation configuration. +- New projects use `navigation.mode: "auto"`; fully static routes ship no WRNexusJS JavaScript. +- Set `navigation.mode: "client"` only when every route requires in-place client navigation. + +## Mandatory project review before production + +Framework controls do not replace application-specific review. Each production project must define and test: + +- Roles, permissions, object ownership, tenant boundaries, and sensitive field access. +- CSP source allowlists and third-party scripts. +- Trusted proxies, public hosts, CORS origins, WebSocket origins, and outbound URL allowlists. +- Session duration, MFA/passkey requirements, recovery policy, and high-risk reauthentication. +- Upload type policy, storage isolation, antivirus/CDR adapters, quotas, and retention. +- Cache privacy and invalidation for authenticated or tenant-specific data. +- Database indexes, pool sizes, query limits, backups, and migration rollback. +- Observability retention, sampling, redaction, and incident alerts. +- CDN/TLS configuration, registry MFA, short-lived publish credentials, and disaster recovery. + +## Production gates + +```sh +bun install +bun run validate:0.7 +bun run security:framework +bun run sbom +bun run benchmark:framework +bun run validate:staging +bun run check +bun run release:prepare +``` + +Run `release:private` only after generated files and release changes are committed, pushed, and the working tree is clean. diff --git a/LOCAL-VALIDATION-REQUIRED-0.7.0.md b/LOCAL-VALIDATION-REQUIRED-0.7.0.md new file mode 100644 index 00000000..2241cd1e --- /dev/null +++ b/LOCAL-VALIDATION-REQUIRED-0.7.0.md @@ -0,0 +1,28 @@ +# WRNexusJS 0.7.0 local validation requirement + +The packaging environment does not provide Bun or the repository's installed ESLint/Prettier dependencies. Source-only TypeScript, Node-compatible validators, compiler sweeps, security probes, benchmarks, SBOM generation, package-stage integrity probes, and VS Code tests were completed here. + +The authoritative complete validation must run in the target Windows repository with Bun 1.3 or newer: + +```powershell +bun install +bun run validate:0.7 +bun run security:framework +bun run sbom +bun run benchmark:framework +bun run validate:staging +bun run check +``` + +`bun run check` is required because it executes the complete repository TypeScript, ESLint, Bun test, and Prettier checks with the actual dependency graph. + +Before publication: + +```powershell +bun run release:prepare +git status +git push origin main +bun run release:private +``` + +Do not publish if the working tree is dirty, the generated UI reference changes, a benchmark budget fails, the security audit reports an error, or staged package integrity verification fails. diff --git a/PUBLISHING-0.7.0.md b/PUBLISHING-0.7.0.md new file mode 100644 index 00000000..93f29567 --- /dev/null +++ b/PUBLISHING-0.7.0.md @@ -0,0 +1,27 @@ +# Publishing WRNexusJS 0.7.0 privately + +Run the production gates before staging packages: + +```sh +bun install +bun run validate:0.7 +bun run security:framework +bun run sbom +bun run benchmark:framework +bun run validate:staging +bun run check +``` + +Prepare, commit, and publish only from a clean branch: + +```sh +bun run release:prepare +git add --all +git commit -m "release: WRNexusJS 0.7.0 security and performance foundation" +git push origin main +bun run release:private +``` + +The release workflow validates the 0.7 migration, framework security audit, deterministic CycloneDX SBOM, benchmark budgets, full type/lint/test/format checks, deterministic UI references, restricted package staging, package SHA-256 manifests, clean Git state, and private-registry policy. + +The staging verifier rejects secrets, private keys, source maps, TypeScript source files other than declarations, and unexpected environment files before publication. diff --git a/RELEASE_NOTES-0.7.0.md b/RELEASE_NOTES-0.7.0.md new file mode 100644 index 00000000..5fdbc133 --- /dev/null +++ b/RELEASE_NOTES-0.7.0.md @@ -0,0 +1,52 @@ +# WRNexusJS 0.7.0 — Security and Performance Foundation + +WRNexusJS 0.7.0 implements the framework-wide production hardening program across compiler security, SSR serialization, headers, CSRF, sessions, gateway limits, SSRF, uploads, database instrumentation, static runtime analysis, hydration telemetry, caching, images, HTTP compression, realtime controls, observability, DevToolbar, release gates, SBOM generation, and benchmarks. + +## New packages + +`@wrnexus/security`, `@wrnexus/cache`, `@wrnexus/image`, `@wrnexus/observability`, and `@wrnexus/benchmark`. + +## Compatibility + +The release keeps existing 0.6 syntax and component/runtime contracts. The updater adds the new packages and inserts production defaults only when the corresponding top-level config section is absent. Existing custom security, performance, observability, and navigation sections are preserved. + +The new default navigation mode is `auto`: fully static pages ship no framework JavaScript and use native document navigation. Set `navigation.mode: "client"` to retain client navigation on every route. + +## Required validation + +```sh +bun install +bun run validate:0.7 +bun run security:framework +bun run benchmark:framework +bun run check +``` + +## 0.7.0 R2 typecheck correction + +- Removed leaked focused-typecheck shim files that polluted the root TypeScript program. +- Added a validation gate preventing those temporary files from entering future archives. + +## 0.7.0 R3 workspace-state correction + +- Added a safe workspace repair command for stale files that survive ZIP overlays. +- Root TypeScript explicitly excludes focused-typecheck helper declarations. +- Local `.env` contents are retained while unsafe files are removed from Git tracking. +- Security, release, and package-staging gates now reject tracked typecheck shims. +- Added a Git-backed workspace repair regression test. + +## 0.7.0 R4 Happy DOM test typing correction + +- Derived synthetic validation event types from Happy DOM's own `dispatchEvent` signature. +- Fixed the two remaining strict TypeScript errors in validation tests. +- Clarified workspace check output with `would remove` wording in dry-run mode. +- Added a validator guard for the Happy DOM event type contract. + +## R6 lint cleanup + +- Removed lint-blocked control-character regexes from URL security paths. +- Enabled secure store hydration serialization. +- Preserved caught SBOM parse errors with `cause`. +- Fixed Node global handling in the workspace repair test. +- Isolated local focused-typecheck leftovers from lint and formatting. +- Added permanent `validate:0.7` regression gates. diff --git a/SBOM-0.7.0.cdx.json b/SBOM-0.7.0.cdx.json new file mode 100644 index 00000000..ea6d5604 --- /dev/null +++ b/SBOM-0.7.0.cdx.json @@ -0,0 +1,6357 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:7f3be7ab-ff7a-505e-a9dd-237f5b8f9a0f", + "version": 1, + "metadata": { + "component": { + "type": "framework", + "name": "WRNexusJS", + "version": "0.7.0", + "bom-ref": "pkg:generic/WRNexusJS@0.7.0" + } + }, + "components": [ + { + "type": "library", + "name": "@antfu/install-pkg", + "version": "1.1.0", + "bom-ref": "pkg:npm/%40antfu/install-pkg@1.1.0", + "purl": "pkg:npm/%40antfu/install-pkg@1.1.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==" + } + ] + }, + { + "type": "library", + "name": "@cyberalien/svg-utils", + "version": "1.2.19", + "bom-ref": "pkg:npm/%40cyberalien/svg-utils@1.2.19", + "purl": "pkg:npm/%40cyberalien/svg-utils@1.2.19", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "paDJoDu+LhuH5Ma1q0BDqBpg6d16Xk22cZZrlg/s/J9oB8KgnnpU/snaEETJu2G1pwYcWQINUz6WUqu0Wm9k6A==" + } + ] + }, + { + "type": "library", + "name": "@emnapi/core", + "version": "1.11.1", + "bom-ref": "pkg:npm/%40emnapi/core@1.11.1", + "purl": "pkg:npm/%40emnapi/core@1.11.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==" + } + ] + }, + { + "type": "library", + "name": "@emnapi/runtime", + "version": "1.11.1", + "bom-ref": "pkg:npm/%40emnapi/runtime@1.11.1", + "purl": "pkg:npm/%40emnapi/runtime@1.11.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==" + } + ] + }, + { + "type": "library", + "name": "@emnapi/wasi-threads", + "version": "1.2.2", + "bom-ref": "pkg:npm/%40emnapi/wasi-threads@1.2.2", + "purl": "pkg:npm/%40emnapi/wasi-threads@1.2.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/aix-ppc64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/aix-ppc64@0.28.1", + "purl": "pkg:npm/%40esbuild/aix-ppc64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/android-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/android-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/android-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/android-arm", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/android-arm@0.28.1", + "purl": "pkg:npm/%40esbuild/android-arm@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/android-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/android-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/android-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/darwin-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/darwin-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/darwin-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/darwin-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/darwin-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/darwin-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/freebsd-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/freebsd-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/freebsd-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/freebsd-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/freebsd-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/freebsd-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-arm", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-arm@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-arm@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-ia32", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-ia32@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-ia32@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-loong64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-loong64@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-loong64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-mips64el", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-mips64el@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-mips64el@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-ppc64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-ppc64@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-ppc64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-riscv64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-riscv64@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-riscv64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-s390x", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-s390x@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-s390x@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/linux-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/linux-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/linux-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/netbsd-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/netbsd-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/netbsd-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/netbsd-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/netbsd-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/netbsd-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/openbsd-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/openbsd-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/openbsd-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/openbsd-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/openbsd-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/openbsd-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/openharmony-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/openharmony-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/openharmony-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/sunos-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/sunos-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/sunos-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/win32-arm64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/win32-arm64@0.28.1", + "purl": "pkg:npm/%40esbuild/win32-arm64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/win32-ia32", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/win32-ia32@0.28.1", + "purl": "pkg:npm/%40esbuild/win32-ia32@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==" + } + ] + }, + { + "type": "library", + "name": "@esbuild/win32-x64", + "version": "0.28.1", + "bom-ref": "pkg:npm/%40esbuild/win32-x64@0.28.1", + "purl": "pkg:npm/%40esbuild/win32-x64@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==" + } + ] + }, + { + "type": "library", + "name": "@eslint-community/eslint-utils", + "version": "4.9.1", + "bom-ref": "pkg:npm/%40eslint-community/eslint-utils@4.9.1", + "purl": "pkg:npm/%40eslint-community/eslint-utils@4.9.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==" + } + ] + }, + { + "type": "library", + "name": "@eslint-community/regexpp", + "version": "4.12.2", + "bom-ref": "pkg:npm/%40eslint-community/regexpp@4.12.2", + "purl": "pkg:npm/%40eslint-community/regexpp@4.12.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==" + } + ] + }, + { + "type": "library", + "name": "@eslint/config-array", + "version": "0.23.5", + "bom-ref": "pkg:npm/%40eslint/config-array@0.23.5", + "purl": "pkg:npm/%40eslint/config-array@0.23.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==" + } + ] + }, + { + "type": "library", + "name": "@eslint/config-helpers", + "version": "0.6.0", + "bom-ref": "pkg:npm/%40eslint/config-helpers@0.6.0", + "purl": "pkg:npm/%40eslint/config-helpers@0.6.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==" + } + ] + }, + { + "type": "library", + "name": "@eslint/config-helpers", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40eslint/config-helpers@0.7.0", + "purl": "pkg:npm/%40eslint/config-helpers@0.7.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==" + } + ] + }, + { + "type": "library", + "name": "@eslint/core", + "version": "1.2.1", + "bom-ref": "pkg:npm/%40eslint/core@1.2.1", + "purl": "pkg:npm/%40eslint/core@1.2.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==" + } + ] + }, + { + "type": "library", + "name": "@eslint/js", + "version": "10.0.1", + "bom-ref": "pkg:npm/%40eslint/js@10.0.1", + "purl": "pkg:npm/%40eslint/js@10.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==" + } + ] + }, + { + "type": "library", + "name": "@eslint/object-schema", + "version": "3.0.5", + "bom-ref": "pkg:npm/%40eslint/object-schema@3.0.5", + "purl": "pkg:npm/%40eslint/object-schema@3.0.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==" + } + ] + }, + { + "type": "library", + "name": "@eslint/plugin-kit", + "version": "0.7.2", + "bom-ref": "pkg:npm/%40eslint/plugin-kit@0.7.2", + "purl": "pkg:npm/%40eslint/plugin-kit@0.7.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==" + } + ] + }, + { + "type": "library", + "name": "@humanfs/core", + "version": "0.19.2", + "bom-ref": "pkg:npm/%40humanfs/core@0.19.2", + "purl": "pkg:npm/%40humanfs/core@0.19.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==" + } + ] + }, + { + "type": "library", + "name": "@humanfs/node", + "version": "0.16.8", + "bom-ref": "pkg:npm/%40humanfs/node@0.16.8", + "purl": "pkg:npm/%40humanfs/node@0.16.8", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==" + } + ] + }, + { + "type": "library", + "name": "@humanfs/types", + "version": "0.15.0", + "bom-ref": "pkg:npm/%40humanfs/types@0.15.0", + "purl": "pkg:npm/%40humanfs/types@0.15.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==" + } + ] + }, + { + "type": "library", + "name": "@humanwhocodes/module-importer", + "version": "1.0.1", + "bom-ref": "pkg:npm/%40humanwhocodes/module-importer@1.0.1", + "purl": "pkg:npm/%40humanwhocodes/module-importer@1.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + } + ] + }, + { + "type": "library", + "name": "@humanwhocodes/retry", + "version": "0.4.3", + "bom-ref": "pkg:npm/%40humanwhocodes/retry@0.4.3", + "purl": "pkg:npm/%40humanwhocodes/retry@0.4.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==" + } + ] + }, + { + "type": "library", + "name": "@iconify-json/lucide", + "version": "1.2.118", + "bom-ref": "pkg:npm/%40iconify-json/lucide@1.2.118", + "purl": "pkg:npm/%40iconify-json/lucide@1.2.118", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JBnK4YOq6K/lA0JP//27QxFxJ4120TjvfXAzGZZIGjCcXcRRRFxl1rcV7+IWdcVCe90KXdqVaAwLaLf6G3HELw==" + } + ] + }, + { + "type": "library", + "name": "@iconify-json/lucide", + "version": "1.2.119", + "bom-ref": "pkg:npm/%40iconify-json/lucide@1.2.119", + "purl": "pkg:npm/%40iconify-json/lucide@1.2.119", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "KbrC7fT5wPshV1KLRM0k91poP0OH4eKEVywEOH0FCczKb+qth8Pcq1Hy9kS2tXeF52s23qzLnG8AJsExPSupeA==" + } + ] + }, + { + "type": "library", + "name": "@iconify/tailwind4", + "version": "1.2.3", + "bom-ref": "pkg:npm/%40iconify/tailwind4@1.2.3", + "purl": "pkg:npm/%40iconify/tailwind4@1.2.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA==" + } + ] + }, + { + "type": "library", + "name": "@iconify/tools", + "version": "5.0.12", + "bom-ref": "pkg:npm/%40iconify/tools@5.0.12", + "purl": "pkg:npm/%40iconify/tools@5.0.12", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "aFPwSFmFphUPVjNLUkgxgUPSPVgTEEjv0mE7NgvfoQBuE5TtsMrKa4HTY65cM5oXZ2a1xKQcW/RKhiOKEiAarw==" + } + ] + }, + { + "type": "library", + "name": "@iconify/types", + "version": "2.0.0", + "bom-ref": "pkg:npm/%40iconify/types@2.0.0", + "purl": "pkg:npm/%40iconify/types@2.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" + } + ] + }, + { + "type": "library", + "name": "@iconify/utils", + "version": "3.1.4", + "bom-ref": "pkg:npm/%40iconify/utils@3.1.4", + "purl": "pkg:npm/%40iconify/utils@3.1.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==" + } + ] + }, + { + "type": "library", + "name": "@jridgewell/gen-mapping", + "version": "0.3.13", + "bom-ref": "pkg:npm/%40jridgewell/gen-mapping@0.3.13", + "purl": "pkg:npm/%40jridgewell/gen-mapping@0.3.13", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==" + } + ] + }, + { + "type": "library", + "name": "@jridgewell/remapping", + "version": "2.3.5", + "bom-ref": "pkg:npm/%40jridgewell/remapping@2.3.5", + "purl": "pkg:npm/%40jridgewell/remapping@2.3.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==" + } + ] + }, + { + "type": "library", + "name": "@jridgewell/resolve-uri", + "version": "3.1.2", + "bom-ref": "pkg:npm/%40jridgewell/resolve-uri@3.1.2", + "purl": "pkg:npm/%40jridgewell/resolve-uri@3.1.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + } + ] + }, + { + "type": "library", + "name": "@jridgewell/sourcemap-codec", + "version": "1.5.5", + "bom-ref": "pkg:npm/%40jridgewell/sourcemap-codec@1.5.5", + "purl": "pkg:npm/%40jridgewell/sourcemap-codec@1.5.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + } + ] + }, + { + "type": "library", + "name": "@jridgewell/trace-mapping", + "version": "0.3.31", + "bom-ref": "pkg:npm/%40jridgewell/trace-mapping@0.3.31", + "purl": "pkg:npm/%40jridgewell/trace-mapping@0.3.31", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==" + } + ] + }, + { + "type": "library", + "name": "@napi-rs/wasm-runtime", + "version": "1.1.6", + "bom-ref": "pkg:npm/%40napi-rs/wasm-runtime@1.1.6", + "purl": "pkg:npm/%40napi-rs/wasm-runtime@1.1.6", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-android-arm64", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-android-arm64@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-android-arm64@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-darwin-arm64", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-darwin-arm64@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-darwin-arm64@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-darwin-x64", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-darwin-x64@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-darwin-x64@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-freebsd-x64", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-freebsd-x64@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-freebsd-x64@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-linux-arm-glibc", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-linux-arm-glibc@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-linux-arm-glibc@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-linux-arm-musl", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-linux-arm-musl@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-linux-arm-musl@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-linux-arm64-glibc", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-linux-arm64-glibc@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-linux-arm64-glibc@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-linux-arm64-musl", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-linux-arm64-musl@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-linux-arm64-musl@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-linux-x64-glibc", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-linux-x64-glibc@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-linux-x64-glibc@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-linux-x64-musl", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-linux-x64-musl@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-linux-x64-musl@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-win32-arm64", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-win32-arm64@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-win32-arm64@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-win32-ia32", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-win32-ia32@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-win32-ia32@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher-win32-x64", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher-win32-x64@2.5.1", + "purl": "pkg:npm/%40parcel/watcher-win32-x64@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==" + } + ] + }, + { + "type": "library", + "name": "@parcel/watcher", + "version": "2.5.1", + "bom-ref": "pkg:npm/%40parcel/watcher@2.5.1", + "purl": "pkg:npm/%40parcel/watcher@2.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-android-arm-eabi", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-android-arm-eabi@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-android-arm-eabi@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-android-arm64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-android-arm64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-android-arm64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-darwin-arm64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-darwin-arm64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-darwin-arm64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-darwin-x64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-darwin-x64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-darwin-x64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-freebsd-arm64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-freebsd-arm64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-freebsd-arm64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-freebsd-x64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-freebsd-x64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-freebsd-x64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-arm-gnueabihf", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-arm-gnueabihf@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-arm-gnueabihf@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-arm-musleabihf", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-arm-musleabihf@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-arm-musleabihf@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-arm64-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-arm64-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-arm64-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-arm64-musl", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-arm64-musl@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-arm64-musl@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-loong64-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-loong64-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-loong64-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-loong64-musl", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-loong64-musl@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-loong64-musl@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-ppc64-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-ppc64-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-ppc64-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-ppc64-musl", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-ppc64-musl@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-ppc64-musl@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-riscv64-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-riscv64-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-riscv64-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-riscv64-musl", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-riscv64-musl@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-riscv64-musl@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-s390x-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-s390x-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-s390x-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-x64-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-x64-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-x64-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-linux-x64-musl", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-linux-x64-musl@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-linux-x64-musl@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-openbsd-x64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-openbsd-x64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-openbsd-x64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-openharmony-arm64", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-openharmony-arm64@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-openharmony-arm64@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-win32-arm64-msvc", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-win32-arm64-msvc@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-win32-arm64-msvc@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-win32-ia32-msvc", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-win32-ia32-msvc@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-win32-ia32-msvc@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-win32-x64-gnu", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-win32-x64-gnu@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-win32-x64-gnu@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==" + } + ] + }, + { + "type": "library", + "name": "@rollup/rollup-win32-x64-msvc", + "version": "4.62.2", + "bom-ref": "pkg:npm/%40rollup/rollup-win32-x64-msvc@4.62.2", + "purl": "pkg:npm/%40rollup/rollup-win32-x64-msvc@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/cli", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/cli@4.3.2", + "purl": "pkg:npm/%40tailwindcss/cli@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Fzt+HrIZHDlkRYKdLMBeufaroaPvwCBG70sMLdmurdeadNMO/LxbmT8Sbb+P83ep0iAlAImettb7Y+rO+37rXw==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/node", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/node@4.3.2", + "purl": "pkg:npm/%40tailwindcss/node@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-android-arm64", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-android-arm64@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-android-arm64@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-darwin-arm64", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-darwin-arm64@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-darwin-arm64@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-darwin-x64", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-darwin-x64@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-darwin-x64@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-freebsd-x64", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-freebsd-x64@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-freebsd-x64@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-linux-arm-gnueabihf", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-linux-arm-gnueabihf@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-linux-arm64-gnu", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-linux-arm64-gnu@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-linux-arm64-gnu@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-linux-arm64-musl", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-linux-arm64-musl@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-linux-arm64-musl@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-linux-x64-gnu", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-linux-x64-gnu@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-linux-x64-gnu@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-linux-x64-musl", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-linux-x64-musl@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-linux-x64-musl@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-wasm32-wasi", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-wasm32-wasi@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-wasm32-wasi@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-win32-arm64-msvc", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-win32-arm64-msvc@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-win32-arm64-msvc@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide-win32-x64-msvc", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide-win32-x64-msvc@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide-win32-x64-msvc@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==" + } + ] + }, + { + "type": "library", + "name": "@tailwindcss/oxide", + "version": "4.3.2", + "bom-ref": "pkg:npm/%40tailwindcss/oxide@4.3.2", + "purl": "pkg:npm/%40tailwindcss/oxide@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==" + } + ] + }, + { + "type": "library", + "name": "@tybys/wasm-util", + "version": "0.10.3", + "bom-ref": "pkg:npm/%40tybys/wasm-util@0.10.3", + "purl": "pkg:npm/%40tybys/wasm-util@0.10.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==" + } + ] + }, + { + "type": "library", + "name": "@types/bun", + "version": "1.3.14", + "bom-ref": "pkg:npm/%40types/bun@1.3.14", + "purl": "pkg:npm/%40types/bun@1.3.14", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==" + } + ] + }, + { + "type": "library", + "name": "@types/esrecurse", + "version": "4.3.1", + "bom-ref": "pkg:npm/%40types/esrecurse@4.3.1", + "purl": "pkg:npm/%40types/esrecurse@4.3.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==" + } + ] + }, + { + "type": "library", + "name": "@types/estree", + "version": "1.0.9", + "bom-ref": "pkg:npm/%40types/estree@1.0.9", + "purl": "pkg:npm/%40types/estree@1.0.9", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" + } + ] + }, + { + "type": "library", + "name": "@types/json-schema", + "version": "7.0.15", + "bom-ref": "pkg:npm/%40types/json-schema@7.0.15", + "purl": "pkg:npm/%40types/json-schema@7.0.15", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + } + ] + }, + { + "type": "library", + "name": "@types/node", + "version": "26.0.1", + "bom-ref": "pkg:npm/%40types/node@26.0.1", + "purl": "pkg:npm/%40types/node@26.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==" + } + ] + }, + { + "type": "library", + "name": "@types/whatwg-mimetype", + "version": "3.0.2", + "bom-ref": "pkg:npm/%40types/whatwg-mimetype@3.0.2", + "purl": "pkg:npm/%40types/whatwg-mimetype@3.0.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==" + } + ] + }, + { + "type": "library", + "name": "@types/ws", + "version": "8.18.1", + "bom-ref": "pkg:npm/%40types/ws@8.18.1", + "purl": "pkg:npm/%40types/ws@8.18.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/eslint-plugin", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/eslint-plugin@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/eslint-plugin@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "4EQM77WgVNxj7OkL/5b/D/xZsw00G577+UriYTC7JF5opcF3T2AuoeY7ueLaZgSVjSgCS6yOAJB5bRGLPSJUzA==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/eslint-plugin", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/eslint-plugin@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/eslint-plugin@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/parser", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/parser@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/parser@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "sPhE4iHuJDSvoAiec+Ro8JyXw8f0ql13HFR82P99nCm9GwTEKG0KYLvDe6REk8BCXuit6vJAv/Yxg5ABaNS2rA==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/parser", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/parser@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/parser@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/project-service", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/project-service@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/project-service@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yQ3RgY5RkSBpsNS1Bx/JQEcA24FOSdfGktoyprAr5u18390UQdtVcfnEv4nIrIshNnavlVyZBKxQwT1fIAE6cg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/project-service", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/project-service@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/project-service@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/scope-manager", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/scope-manager@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/scope-manager@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "r4d249KbQ1SFdpeStvob8Ih6aPPIzfqllPVOtvhve6ZcpuVcYo5/7zUWckKpHE7StASX4kTKZTLf0WQm/wPkcg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/scope-manager", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/scope-manager@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/scope-manager@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/tsconfig-utils", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/tsconfig-utils@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/tsconfig-utils@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xadytJqX9vJVQ2fdQjkcIVigwaOJNWkpjdLt6cEQ+xPnrI1fkp+/jZE/I97k9KUjqtpd25i0HeyZf3T6dutv2g==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/tsconfig-utils", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/tsconfig-utils@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/tsconfig-utils@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/type-utils", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/type-utils@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/type-utils@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "aXM5xlqXiTxPibXB93cLAURfT3rlizf7uMXISCXy66Isr/9hISJx3yDsKl0L7lKa51b8JpFuNKby0/O0pEm9jg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/type-utils", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/type-utils@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/type-utils@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/types", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/types@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/types@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ooCzJFaf+Hg+uG6fA3NRFGuFjlfNlDhBthbv4ZPU/0elCAFUfnyXUvf/WOpHz/jYwSmvU2GkR2LtyUfy1AxZ1Q==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/types", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/types@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/types@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/typescript-estree", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/typescript-estree@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/typescript-estree@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xMcW9oP9u7fAMXYs9A65CVmtLQe2r//oXINHfi8HV+oiqhih17sbLdhXr4540YWlgpDKQdY854OL5ZrdCiQsAA==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/typescript-estree", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/typescript-estree@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/typescript-estree@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/utils", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/utils@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/utils@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "sHtbPfuKNZCG+ih8SyjjucqRntSVmp8XgL5u6o9mAhiSn8ds5o/M/XdM0abweme2Tln3szOstOrZ9OXitvPh0g==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/utils", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/utils@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/utils@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/visitor-keys", + "version": "8.62.1", + "bom-ref": "pkg:npm/%40typescript-eslint/visitor-keys@8.62.1", + "purl": "pkg:npm/%40typescript-eslint/visitor-keys@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "4g3BLxfdTMy8iZG0MaBkadnlRrCJ74cQiFbyEVMrkwIoqdyaXXQM22cotDvrl4x28wgIZ9rEJRoM+mmhSJpJ1g==" + } + ] + }, + { + "type": "library", + "name": "@typescript-eslint/visitor-keys", + "version": "8.65.0", + "bom-ref": "pkg:npm/%40typescript-eslint/visitor-keys@8.65.0", + "purl": "pkg:npm/%40typescript-eslint/visitor-keys@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/ai", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/ai@0.7.0", + "purl": "pkg:npm/%40wrnexus/ai@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/auth", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/auth@0.7.0", + "purl": "pkg:npm/%40wrnexus/auth@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/authz", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/authz@0.7.0", + "purl": "pkg:npm/%40wrnexus/authz@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/benchmark", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/benchmark@0.7.0", + "purl": "pkg:npm/%40wrnexus/benchmark@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/cache", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/cache@0.7.0", + "purl": "pkg:npm/%40wrnexus/cache@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/captcha", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/captcha@0.7.0", + "purl": "pkg:npm/%40wrnexus/captcha@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/cli", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/cli@0.7.0", + "purl": "pkg:npm/%40wrnexus/cli@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/compiler", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/compiler@0.7.0", + "purl": "pkg:npm/%40wrnexus/compiler@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/core", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/core@0.7.0", + "purl": "pkg:npm/%40wrnexus/core@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/csr", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/csr@0.7.0", + "purl": "pkg:npm/%40wrnexus/csr@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/db", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/db@0.7.0", + "purl": "pkg:npm/%40wrnexus/db@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/dev-server", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/dev-server@0.7.0", + "purl": "pkg:npm/%40wrnexus/dev-server@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/dev-toolbar", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/dev-toolbar@0.7.0", + "purl": "pkg:npm/%40wrnexus/dev-toolbar@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/encryption", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/encryption@0.7.0", + "purl": "pkg:npm/%40wrnexus/encryption@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/helpers", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/helpers@0.7.0", + "purl": "pkg:npm/%40wrnexus/helpers@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/i18n", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/i18n@0.7.0", + "purl": "pkg:npm/%40wrnexus/i18n@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/image", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/image@0.7.0", + "purl": "pkg:npm/%40wrnexus/image@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/jwt", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/jwt@0.7.0", + "purl": "pkg:npm/%40wrnexus/jwt@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/mobile", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/mobile@0.7.0", + "purl": "pkg:npm/%40wrnexus/mobile@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/native", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/native@0.7.0", + "purl": "pkg:npm/%40wrnexus/native@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/oauth", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/oauth@0.7.0", + "purl": "pkg:npm/%40wrnexus/oauth@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/observability", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/observability@0.7.0", + "purl": "pkg:npm/%40wrnexus/observability@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/plugin", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/plugin@0.7.0", + "purl": "pkg:npm/%40wrnexus/plugin@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/pubsub", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/pubsub@0.7.0", + "purl": "pkg:npm/%40wrnexus/pubsub@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/queue", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/queue@0.7.0", + "purl": "pkg:npm/%40wrnexus/queue@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/reactive", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/reactive@0.7.0", + "purl": "pkg:npm/%40wrnexus/reactive@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/router", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/router@0.7.0", + "purl": "pkg:npm/%40wrnexus/router@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/security", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/security@0.7.0", + "purl": "pkg:npm/%40wrnexus/security@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/ssr", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/ssr@0.7.0", + "purl": "pkg:npm/%40wrnexus/ssr@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/store", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/store@0.7.0", + "purl": "pkg:npm/%40wrnexus/store@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/styles", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/styles@0.7.0", + "purl": "pkg:npm/%40wrnexus/styles@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/syntax", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/syntax@0.7.0", + "purl": "pkg:npm/%40wrnexus/syntax@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/test", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/test@0.7.0", + "purl": "pkg:npm/%40wrnexus/test@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/tracking", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/tracking@0.7.0", + "purl": "pkg:npm/%40wrnexus/tracking@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/typecheck", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/typecheck@0.7.0", + "purl": "pkg:npm/%40wrnexus/typecheck@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/ui", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/ui@0.7.0", + "purl": "pkg:npm/%40wrnexus/ui@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/uploader", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/uploader@0.7.0", + "purl": "pkg:npm/%40wrnexus/uploader@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "@wrnexus/validation", + "version": "0.7.0", + "bom-ref": "pkg:npm/%40wrnexus/validation@0.7.0", + "purl": "pkg:npm/%40wrnexus/validation@0.7.0", + "properties": [ + { + "name": "wrnexus:workspace", + "value": "true" + }, + { + "name": "wrnexus:private", + "value": "true" + } + ] + }, + { + "type": "library", + "name": "acorn-jsx", + "version": "5.3.2", + "bom-ref": "pkg:npm/acorn-jsx@5.3.2", + "purl": "pkg:npm/acorn-jsx@5.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==" + } + ] + }, + { + "type": "library", + "name": "acorn", + "version": "8.17.0", + "bom-ref": "pkg:npm/acorn@8.17.0", + "purl": "pkg:npm/acorn@8.17.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==" + } + ] + }, + { + "type": "library", + "name": "ajv", + "version": "6.15.0", + "bom-ref": "pkg:npm/ajv@6.15.0", + "purl": "pkg:npm/ajv@6.15.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==" + } + ] + }, + { + "type": "library", + "name": "any-promise", + "version": "1.3.0", + "bom-ref": "pkg:npm/any-promise@1.3.0", + "purl": "pkg:npm/any-promise@1.3.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + } + ] + }, + { + "type": "library", + "name": "auth-showcase", + "version": "workspace:examples/auth-showcase", + "bom-ref": "pkg:npm/auth-showcase@workspace:examples/auth-showcase", + "purl": "pkg:npm/auth-showcase@workspace:examples/auth-showcase", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ] + }, + { + "type": "library", + "name": "balanced-match", + "version": "4.0.4", + "bom-ref": "pkg:npm/balanced-match@4.0.4", + "purl": "pkg:npm/balanced-match@4.0.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==" + } + ] + }, + { + "type": "library", + "name": "basic-app", + "version": "workspace:examples/basic-app", + "bom-ref": "pkg:npm/basic-app@workspace:examples/basic-app", + "purl": "pkg:npm/basic-app@workspace:examples/basic-app", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ] + }, + { + "type": "library", + "name": "boolbase", + "version": "1.0.0", + "bom-ref": "pkg:npm/boolbase@1.0.0", + "purl": "pkg:npm/boolbase@1.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + } + ] + }, + { + "type": "library", + "name": "brace-expansion", + "version": "5.0.7", + "bom-ref": "pkg:npm/brace-expansion@5.0.7", + "purl": "pkg:npm/brace-expansion@5.0.7", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==" + } + ] + }, + { + "type": "library", + "name": "braces", + "version": "3.0.3", + "bom-ref": "pkg:npm/braces@3.0.3", + "purl": "pkg:npm/braces@3.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==" + } + ] + }, + { + "type": "library", + "name": "buffer-image-size", + "version": "0.6.4", + "bom-ref": "pkg:npm/buffer-image-size@0.6.4", + "purl": "pkg:npm/buffer-image-size@0.6.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ==" + } + ] + }, + { + "type": "library", + "name": "bun-types", + "version": "1.3.14", + "bom-ref": "pkg:npm/bun-types@1.3.14", + "purl": "pkg:npm/bun-types@1.3.14", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==" + } + ] + }, + { + "type": "library", + "name": "bundle-require", + "version": "5.1.0", + "bom-ref": "pkg:npm/bundle-require@5.1.0", + "purl": "pkg:npm/bundle-require@5.1.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==" + } + ] + }, + { + "type": "library", + "name": "cac", + "version": "6.7.14", + "bom-ref": "pkg:npm/cac@6.7.14", + "purl": "pkg:npm/cac@6.7.14", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" + } + ] + }, + { + "type": "library", + "name": "captcha-showcase", + "version": "workspace:examples/captcha-showcase", + "bom-ref": "pkg:npm/captcha-showcase@workspace:examples/captcha-showcase", + "purl": "pkg:npm/captcha-showcase@workspace:examples/captcha-showcase", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ] + }, + { + "type": "library", + "name": "chokidar", + "version": "4.0.3", + "bom-ref": "pkg:npm/chokidar@4.0.3", + "purl": "pkg:npm/chokidar@4.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==" + } + ] + }, + { + "type": "library", + "name": "commander", + "version": "11.1.0", + "bom-ref": "pkg:npm/commander@11.1.0", + "purl": "pkg:npm/commander@11.1.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + } + ] + }, + { + "type": "library", + "name": "commander", + "version": "4.1.1", + "bom-ref": "pkg:npm/commander@4.1.1", + "purl": "pkg:npm/commander@4.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" + } + ] + }, + { + "type": "library", + "name": "component-showcase", + "version": "workspace:examples/component-showcase", + "bom-ref": "pkg:npm/component-showcase@workspace:examples/component-showcase", + "purl": "pkg:npm/component-showcase@workspace:examples/component-showcase", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ] + }, + { + "type": "library", + "name": "confbox", + "version": "0.1.8", + "bom-ref": "pkg:npm/confbox@0.1.8", + "purl": "pkg:npm/confbox@0.1.8", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==" + } + ] + }, + { + "type": "library", + "name": "consola", + "version": "3.4.2", + "bom-ref": "pkg:npm/consola@3.4.2", + "purl": "pkg:npm/consola@3.4.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==" + } + ] + }, + { + "type": "library", + "name": "cross-spawn", + "version": "7.0.6", + "bom-ref": "pkg:npm/cross-spawn@7.0.6", + "purl": "pkg:npm/cross-spawn@7.0.6", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==" + } + ] + }, + { + "type": "library", + "name": "css-select", + "version": "5.2.2", + "bom-ref": "pkg:npm/css-select@5.2.2", + "purl": "pkg:npm/css-select@5.2.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==" + } + ] + }, + { + "type": "library", + "name": "css-tree", + "version": "2.2.1", + "bom-ref": "pkg:npm/css-tree@2.2.1", + "purl": "pkg:npm/css-tree@2.2.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==" + } + ] + }, + { + "type": "library", + "name": "css-tree", + "version": "3.2.1", + "bom-ref": "pkg:npm/css-tree@3.2.1", + "purl": "pkg:npm/css-tree@3.2.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==" + } + ] + }, + { + "type": "library", + "name": "css-what", + "version": "6.2.2", + "bom-ref": "pkg:npm/css-what@6.2.2", + "purl": "pkg:npm/css-what@6.2.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==" + } + ] + }, + { + "type": "library", + "name": "csso", + "version": "5.0.5", + "bom-ref": "pkg:npm/csso@5.0.5", + "purl": "pkg:npm/csso@5.0.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==" + } + ] + }, + { + "type": "library", + "name": "debug", + "version": "4.4.3", + "bom-ref": "pkg:npm/debug@4.4.3", + "purl": "pkg:npm/debug@4.4.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==" + } + ] + }, + { + "type": "library", + "name": "deep-is", + "version": "0.1.4", + "bom-ref": "pkg:npm/deep-is@0.1.4", + "purl": "pkg:npm/deep-is@0.1.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + } + ] + }, + { + "type": "library", + "name": "detect-libc", + "version": "1.0.3", + "bom-ref": "pkg:npm/detect-libc@1.0.3", + "purl": "pkg:npm/detect-libc@1.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + } + ] + }, + { + "type": "library", + "name": "detect-libc", + "version": "2.1.2", + "bom-ref": "pkg:npm/detect-libc@2.1.2", + "purl": "pkg:npm/detect-libc@2.1.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + } + ] + }, + { + "type": "library", + "name": "dom-serializer", + "version": "2.0.0", + "bom-ref": "pkg:npm/dom-serializer@2.0.0", + "purl": "pkg:npm/dom-serializer@2.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==" + } + ] + }, + { + "type": "library", + "name": "domelementtype", + "version": "2.3.0", + "bom-ref": "pkg:npm/domelementtype@2.3.0", + "purl": "pkg:npm/domelementtype@2.3.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" + } + ] + }, + { + "type": "library", + "name": "domhandler", + "version": "5.0.3", + "bom-ref": "pkg:npm/domhandler@5.0.3", + "purl": "pkg:npm/domhandler@5.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==" + } + ] + }, + { + "type": "library", + "name": "domutils", + "version": "3.2.2", + "bom-ref": "pkg:npm/domutils@3.2.2", + "purl": "pkg:npm/domutils@3.2.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==" + } + ] + }, + { + "type": "library", + "name": "enhanced-resolve", + "version": "5.21.6", + "bom-ref": "pkg:npm/enhanced-resolve@5.21.6", + "purl": "pkg:npm/enhanced-resolve@5.21.6", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==" + } + ] + }, + { + "type": "library", + "name": "entities", + "version": "4.5.0", + "bom-ref": "pkg:npm/entities@4.5.0", + "purl": "pkg:npm/entities@4.5.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + ] + }, + { + "type": "library", + "name": "entities", + "version": "7.0.1", + "bom-ref": "pkg:npm/entities@7.0.1", + "purl": "pkg:npm/entities@7.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==" + } + ] + }, + { + "type": "library", + "name": "esbuild", + "version": "0.28.1", + "bom-ref": "pkg:npm/esbuild@0.28.1", + "purl": "pkg:npm/esbuild@0.28.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==" + } + ] + }, + { + "type": "library", + "name": "escape-string-regexp", + "version": "4.0.0", + "bom-ref": "pkg:npm/escape-string-regexp@4.0.0", + "purl": "pkg:npm/escape-string-regexp@4.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + } + ] + }, + { + "type": "library", + "name": "eslint-scope", + "version": "9.1.2", + "bom-ref": "pkg:npm/eslint-scope@9.1.2", + "purl": "pkg:npm/eslint-scope@9.1.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==" + } + ] + }, + { + "type": "library", + "name": "eslint-visitor-keys", + "version": "3.4.3", + "bom-ref": "pkg:npm/eslint-visitor-keys@3.4.3", + "purl": "pkg:npm/eslint-visitor-keys@3.4.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + } + ] + }, + { + "type": "library", + "name": "eslint-visitor-keys", + "version": "5.0.1", + "bom-ref": "pkg:npm/eslint-visitor-keys@5.0.1", + "purl": "pkg:npm/eslint-visitor-keys@5.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==" + } + ] + }, + { + "type": "library", + "name": "eslint", + "version": "10.6.0", + "bom-ref": "pkg:npm/eslint@10.6.0", + "purl": "pkg:npm/eslint@10.6.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==" + } + ] + }, + { + "type": "library", + "name": "eslint", + "version": "10.8.0", + "bom-ref": "pkg:npm/eslint@10.8.0", + "purl": "pkg:npm/eslint@10.8.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==" + } + ] + }, + { + "type": "library", + "name": "espree", + "version": "11.2.0", + "bom-ref": "pkg:npm/espree@11.2.0", + "purl": "pkg:npm/espree@11.2.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==" + } + ] + }, + { + "type": "library", + "name": "esquery", + "version": "1.7.0", + "bom-ref": "pkg:npm/esquery@1.7.0", + "purl": "pkg:npm/esquery@1.7.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==" + } + ] + }, + { + "type": "library", + "name": "esrecurse", + "version": "4.3.0", + "bom-ref": "pkg:npm/esrecurse@4.3.0", + "purl": "pkg:npm/esrecurse@4.3.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==" + } + ] + }, + { + "type": "library", + "name": "estraverse", + "version": "5.3.0", + "bom-ref": "pkg:npm/estraverse@5.3.0", + "purl": "pkg:npm/estraverse@5.3.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + } + ] + }, + { + "type": "library", + "name": "esutils", + "version": "2.0.3", + "bom-ref": "pkg:npm/esutils@2.0.3", + "purl": "pkg:npm/esutils@2.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + } + ] + }, + { + "type": "library", + "name": "fast-deep-equal", + "version": "3.1.3", + "bom-ref": "pkg:npm/fast-deep-equal@3.1.3", + "purl": "pkg:npm/fast-deep-equal@3.1.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + } + ] + }, + { + "type": "library", + "name": "fast-json-stable-stringify", + "version": "2.1.0", + "bom-ref": "pkg:npm/fast-json-stable-stringify@2.1.0", + "purl": "pkg:npm/fast-json-stable-stringify@2.1.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + } + ] + }, + { + "type": "library", + "name": "fast-levenshtein", + "version": "2.0.6", + "bom-ref": "pkg:npm/fast-levenshtein@2.0.6", + "purl": "pkg:npm/fast-levenshtein@2.0.6", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + } + ] + }, + { + "type": "library", + "name": "fdir", + "version": "6.5.0", + "bom-ref": "pkg:npm/fdir@6.5.0", + "purl": "pkg:npm/fdir@6.5.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==" + } + ] + }, + { + "type": "library", + "name": "fflate", + "version": "0.8.3", + "bom-ref": "pkg:npm/fflate@0.8.3", + "purl": "pkg:npm/fflate@0.8.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==" + } + ] + }, + { + "type": "library", + "name": "file-entry-cache", + "version": "8.0.0", + "bom-ref": "pkg:npm/file-entry-cache@8.0.0", + "purl": "pkg:npm/file-entry-cache@8.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==" + } + ] + }, + { + "type": "library", + "name": "fill-range", + "version": "7.1.1", + "bom-ref": "pkg:npm/fill-range@7.1.1", + "purl": "pkg:npm/fill-range@7.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==" + } + ] + }, + { + "type": "library", + "name": "find-up", + "version": "5.0.0", + "bom-ref": "pkg:npm/find-up@5.0.0", + "purl": "pkg:npm/find-up@5.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==" + } + ] + }, + { + "type": "library", + "name": "fix-dts-default-cjs-exports", + "version": "1.0.1", + "bom-ref": "pkg:npm/fix-dts-default-cjs-exports@1.0.1", + "purl": "pkg:npm/fix-dts-default-cjs-exports@1.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==" + } + ] + }, + { + "type": "library", + "name": "flat-cache", + "version": "4.0.1", + "bom-ref": "pkg:npm/flat-cache@4.0.1", + "purl": "pkg:npm/flat-cache@4.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==" + } + ] + }, + { + "type": "library", + "name": "flatted", + "version": "3.4.2", + "bom-ref": "pkg:npm/flatted@3.4.2", + "purl": "pkg:npm/flatted@3.4.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==" + } + ] + }, + { + "type": "library", + "name": "fsevents", + "version": "2.3.3", + "bom-ref": "pkg:npm/fsevents@2.3.3", + "purl": "pkg:npm/fsevents@2.3.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" + } + ] + }, + { + "type": "library", + "name": "glob-parent", + "version": "6.0.2", + "bom-ref": "pkg:npm/glob-parent@6.0.2", + "purl": "pkg:npm/glob-parent@6.0.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==" + } + ] + }, + { + "type": "library", + "name": "graceful-fs", + "version": "4.2.11", + "bom-ref": "pkg:npm/graceful-fs@4.2.11", + "purl": "pkg:npm/graceful-fs@4.2.11", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + } + ] + }, + { + "type": "library", + "name": "happy-dom", + "version": "20.10.6", + "bom-ref": "pkg:npm/happy-dom@20.10.6", + "purl": "pkg:npm/happy-dom@20.10.6", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6QD0ilzDDt93tX44y8tbmZdAcdTRYDhUP+Asgi6pC8Pp5IA3cvaZGyoVN/EGtlq9ziT65iPuBBn3ASLr6hCgVw==" + } + ] + }, + { + "type": "library", + "name": "ignore", + "version": "5.3.2", + "bom-ref": "pkg:npm/ignore@5.3.2", + "purl": "pkg:npm/ignore@5.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==" + } + ] + }, + { + "type": "library", + "name": "ignore", + "version": "7.0.5", + "bom-ref": "pkg:npm/ignore@7.0.5", + "purl": "pkg:npm/ignore@7.0.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==" + } + ] + }, + { + "type": "library", + "name": "import-meta-resolve", + "version": "4.2.0", + "bom-ref": "pkg:npm/import-meta-resolve@4.2.0", + "purl": "pkg:npm/import-meta-resolve@4.2.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==" + } + ] + }, + { + "type": "library", + "name": "imurmurhash", + "version": "0.1.4", + "bom-ref": "pkg:npm/imurmurhash@0.1.4", + "purl": "pkg:npm/imurmurhash@0.1.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + } + ] + }, + { + "type": "library", + "name": "is-extglob", + "version": "2.1.1", + "bom-ref": "pkg:npm/is-extglob@2.1.1", + "purl": "pkg:npm/is-extglob@2.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + } + ] + }, + { + "type": "library", + "name": "is-glob", + "version": "4.0.3", + "bom-ref": "pkg:npm/is-glob@4.0.3", + "purl": "pkg:npm/is-glob@4.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==" + } + ] + }, + { + "type": "library", + "name": "is-number", + "version": "7.0.0", + "bom-ref": "pkg:npm/is-number@7.0.0", + "purl": "pkg:npm/is-number@7.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + } + ] + }, + { + "type": "library", + "name": "isexe", + "version": "2.0.0", + "bom-ref": "pkg:npm/isexe@2.0.0", + "purl": "pkg:npm/isexe@2.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + } + ] + }, + { + "type": "library", + "name": "jiti", + "version": "2.7.0", + "bom-ref": "pkg:npm/jiti@2.7.0", + "purl": "pkg:npm/jiti@2.7.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==" + } + ] + }, + { + "type": "library", + "name": "joycon", + "version": "3.1.1", + "bom-ref": "pkg:npm/joycon@3.1.1", + "purl": "pkg:npm/joycon@3.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==" + } + ] + }, + { + "type": "library", + "name": "json-buffer", + "version": "3.0.1", + "bom-ref": "pkg:npm/json-buffer@3.0.1", + "purl": "pkg:npm/json-buffer@3.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + } + ] + }, + { + "type": "library", + "name": "json-schema-traverse", + "version": "0.4.1", + "bom-ref": "pkg:npm/json-schema-traverse@0.4.1", + "purl": "pkg:npm/json-schema-traverse@0.4.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + } + ] + }, + { + "type": "library", + "name": "json-stable-stringify-without-jsonify", + "version": "1.0.1", + "bom-ref": "pkg:npm/json-stable-stringify-without-jsonify@1.0.1", + "purl": "pkg:npm/json-stable-stringify-without-jsonify@1.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + } + ] + }, + { + "type": "library", + "name": "keyv", + "version": "4.5.4", + "bom-ref": "pkg:npm/keyv@4.5.4", + "purl": "pkg:npm/keyv@4.5.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==" + } + ] + }, + { + "type": "library", + "name": "levn", + "version": "0.4.1", + "bom-ref": "pkg:npm/levn@0.4.1", + "purl": "pkg:npm/levn@0.4.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-android-arm64", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-android-arm64@1.32.0", + "purl": "pkg:npm/lightningcss-android-arm64@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-darwin-arm64", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-darwin-arm64@1.32.0", + "purl": "pkg:npm/lightningcss-darwin-arm64@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-darwin-x64", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-darwin-x64@1.32.0", + "purl": "pkg:npm/lightningcss-darwin-x64@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-freebsd-x64", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-freebsd-x64@1.32.0", + "purl": "pkg:npm/lightningcss-freebsd-x64@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-linux-arm-gnueabihf", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-linux-arm-gnueabihf@1.32.0", + "purl": "pkg:npm/lightningcss-linux-arm-gnueabihf@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-linux-arm64-gnu", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-linux-arm64-gnu@1.32.0", + "purl": "pkg:npm/lightningcss-linux-arm64-gnu@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-linux-arm64-musl", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-linux-arm64-musl@1.32.0", + "purl": "pkg:npm/lightningcss-linux-arm64-musl@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-linux-x64-gnu", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-linux-x64-gnu@1.32.0", + "purl": "pkg:npm/lightningcss-linux-x64-gnu@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-linux-x64-musl", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-linux-x64-musl@1.32.0", + "purl": "pkg:npm/lightningcss-linux-x64-musl@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-win32-arm64-msvc", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-win32-arm64-msvc@1.32.0", + "purl": "pkg:npm/lightningcss-win32-arm64-msvc@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==" + } + ] + }, + { + "type": "library", + "name": "lightningcss-win32-x64-msvc", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss-win32-x64-msvc@1.32.0", + "purl": "pkg:npm/lightningcss-win32-x64-msvc@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==" + } + ] + }, + { + "type": "library", + "name": "lightningcss", + "version": "1.32.0", + "bom-ref": "pkg:npm/lightningcss@1.32.0", + "purl": "pkg:npm/lightningcss@1.32.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==" + } + ] + }, + { + "type": "library", + "name": "lilconfig", + "version": "3.1.3", + "bom-ref": "pkg:npm/lilconfig@3.1.3", + "purl": "pkg:npm/lilconfig@3.1.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==" + } + ] + }, + { + "type": "library", + "name": "lines-and-columns", + "version": "1.2.4", + "bom-ref": "pkg:npm/lines-and-columns@1.2.4", + "purl": "pkg:npm/lines-and-columns@1.2.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + } + ] + }, + { + "type": "library", + "name": "load-tsconfig", + "version": "0.2.5", + "bom-ref": "pkg:npm/load-tsconfig@0.2.5", + "purl": "pkg:npm/load-tsconfig@0.2.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==" + } + ] + }, + { + "type": "library", + "name": "locate-path", + "version": "6.0.0", + "bom-ref": "pkg:npm/locate-path@6.0.0", + "purl": "pkg:npm/locate-path@6.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==" + } + ] + }, + { + "type": "library", + "name": "magic-string", + "version": "0.30.21", + "bom-ref": "pkg:npm/magic-string@0.30.21", + "purl": "pkg:npm/magic-string@0.30.21", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==" + } + ] + }, + { + "type": "library", + "name": "mdn-data", + "version": "2.0.28", + "bom-ref": "pkg:npm/mdn-data@2.0.28", + "purl": "pkg:npm/mdn-data@2.0.28", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + } + ] + }, + { + "type": "library", + "name": "mdn-data", + "version": "2.27.1", + "bom-ref": "pkg:npm/mdn-data@2.27.1", + "purl": "pkg:npm/mdn-data@2.27.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==" + } + ] + }, + { + "type": "library", + "name": "micromatch", + "version": "4.0.8", + "bom-ref": "pkg:npm/micromatch@4.0.8", + "purl": "pkg:npm/micromatch@4.0.8", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==" + } + ] + }, + { + "type": "library", + "name": "minimatch", + "version": "10.2.5", + "bom-ref": "pkg:npm/minimatch@10.2.5", + "purl": "pkg:npm/minimatch@10.2.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==" + } + ] + }, + { + "type": "library", + "name": "mlly", + "version": "1.8.2", + "bom-ref": "pkg:npm/mlly@1.8.2", + "purl": "pkg:npm/mlly@1.8.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==" + } + ] + }, + { + "type": "library", + "name": "modern-tar", + "version": "0.7.7", + "bom-ref": "pkg:npm/modern-tar@0.7.7", + "purl": "pkg:npm/modern-tar@0.7.7", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ==" + } + ] + }, + { + "type": "library", + "name": "mri", + "version": "1.2.0", + "bom-ref": "pkg:npm/mri@1.2.0", + "purl": "pkg:npm/mri@1.2.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + } + ] + }, + { + "type": "library", + "name": "ms", + "version": "2.1.3", + "bom-ref": "pkg:npm/ms@2.1.3", + "purl": "pkg:npm/ms@2.1.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + ] + }, + { + "type": "library", + "name": "mz", + "version": "2.7.0", + "bom-ref": "pkg:npm/mz@2.7.0", + "purl": "pkg:npm/mz@2.7.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==" + } + ] + }, + { + "type": "library", + "name": "natural-compare", + "version": "1.4.0", + "bom-ref": "pkg:npm/natural-compare@1.4.0", + "purl": "pkg:npm/natural-compare@1.4.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + } + ] + }, + { + "type": "library", + "name": "node-addon-api", + "version": "7.1.1", + "bom-ref": "pkg:npm/node-addon-api@7.1.1", + "purl": "pkg:npm/node-addon-api@7.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + } + ] + }, + { + "type": "library", + "name": "nth-check", + "version": "2.1.1", + "bom-ref": "pkg:npm/nth-check@2.1.1", + "purl": "pkg:npm/nth-check@2.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==" + } + ] + }, + { + "type": "library", + "name": "object-assign", + "version": "4.1.1", + "bom-ref": "pkg:npm/object-assign@4.1.1", + "purl": "pkg:npm/object-assign@4.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + } + ] + }, + { + "type": "library", + "name": "optionator", + "version": "0.9.4", + "bom-ref": "pkg:npm/optionator@0.9.4", + "purl": "pkg:npm/optionator@0.9.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==" + } + ] + }, + { + "type": "library", + "name": "p-limit", + "version": "3.1.0", + "bom-ref": "pkg:npm/p-limit@3.1.0", + "purl": "pkg:npm/p-limit@3.1.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==" + } + ] + }, + { + "type": "library", + "name": "p-locate", + "version": "5.0.0", + "bom-ref": "pkg:npm/p-locate@5.0.0", + "purl": "pkg:npm/p-locate@5.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==" + } + ] + }, + { + "type": "library", + "name": "package-manager-detector", + "version": "1.8.0", + "bom-ref": "pkg:npm/package-manager-detector@1.8.0", + "purl": "pkg:npm/package-manager-detector@1.8.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==" + } + ] + }, + { + "type": "library", + "name": "path-exists", + "version": "4.0.0", + "bom-ref": "pkg:npm/path-exists@4.0.0", + "purl": "pkg:npm/path-exists@4.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + } + ] + }, + { + "type": "library", + "name": "path-key", + "version": "3.1.1", + "bom-ref": "pkg:npm/path-key@3.1.1", + "purl": "pkg:npm/path-key@3.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + } + ] + }, + { + "type": "library", + "name": "pathe", + "version": "2.0.3", + "bom-ref": "pkg:npm/pathe@2.0.3", + "purl": "pkg:npm/pathe@2.0.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" + } + ] + }, + { + "type": "library", + "name": "picocolors", + "version": "1.1.1", + "bom-ref": "pkg:npm/picocolors@1.1.1", + "purl": "pkg:npm/picocolors@1.1.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + } + ] + }, + { + "type": "library", + "name": "picomatch", + "version": "2.3.2", + "bom-ref": "pkg:npm/picomatch@2.3.2", + "purl": "pkg:npm/picomatch@2.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==" + } + ] + }, + { + "type": "library", + "name": "picomatch", + "version": "4.0.4", + "bom-ref": "pkg:npm/picomatch@4.0.4", + "purl": "pkg:npm/picomatch@4.0.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==" + } + ] + }, + { + "type": "library", + "name": "pirates", + "version": "4.0.7", + "bom-ref": "pkg:npm/pirates@4.0.7", + "purl": "pkg:npm/pirates@4.0.7", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==" + } + ] + }, + { + "type": "library", + "name": "pkg-types", + "version": "1.3.1", + "bom-ref": "pkg:npm/pkg-types@1.3.1", + "purl": "pkg:npm/pkg-types@1.3.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==" + } + ] + }, + { + "type": "library", + "name": "postcss-load-config", + "version": "6.0.1", + "bom-ref": "pkg:npm/postcss-load-config@6.0.1", + "purl": "pkg:npm/postcss-load-config@6.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==" + } + ] + }, + { + "type": "library", + "name": "prelude-ls", + "version": "1.2.1", + "bom-ref": "pkg:npm/prelude-ls@1.2.1", + "purl": "pkg:npm/prelude-ls@1.2.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + } + ] + }, + { + "type": "library", + "name": "prettier", + "version": "3.9.4", + "bom-ref": "pkg:npm/prettier@3.9.4", + "purl": "pkg:npm/prettier@3.9.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg==" + } + ] + }, + { + "type": "library", + "name": "punycode", + "version": "2.3.1", + "bom-ref": "pkg:npm/punycode@2.3.1", + "purl": "pkg:npm/punycode@2.3.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" + } + ] + }, + { + "type": "library", + "name": "readdirp", + "version": "4.1.2", + "bom-ref": "pkg:npm/readdirp@4.1.2", + "purl": "pkg:npm/readdirp@4.1.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + } + ] + }, + { + "type": "library", + "name": "resolve-from", + "version": "5.0.0", + "bom-ref": "pkg:npm/resolve-from@5.0.0", + "purl": "pkg:npm/resolve-from@5.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==" + } + ] + }, + { + "type": "library", + "name": "rollup", + "version": "4.62.2", + "bom-ref": "pkg:npm/rollup@4.62.2", + "purl": "pkg:npm/rollup@4.62.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==" + } + ] + }, + { + "type": "library", + "name": "sax", + "version": "1.6.0", + "bom-ref": "pkg:npm/sax@1.6.0", + "purl": "pkg:npm/sax@1.6.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==" + } + ] + }, + { + "type": "library", + "name": "semver", + "version": "7.8.5", + "bom-ref": "pkg:npm/semver@7.8.5", + "purl": "pkg:npm/semver@7.8.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==" + } + ] + }, + { + "type": "library", + "name": "shebang-command", + "version": "2.0.0", + "bom-ref": "pkg:npm/shebang-command@2.0.0", + "purl": "pkg:npm/shebang-command@2.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==" + } + ] + }, + { + "type": "library", + "name": "shebang-regex", + "version": "3.0.0", + "bom-ref": "pkg:npm/shebang-regex@3.0.0", + "purl": "pkg:npm/shebang-regex@3.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + } + ] + }, + { + "type": "library", + "name": "source-map-js", + "version": "1.2.1", + "bom-ref": "pkg:npm/source-map-js@1.2.1", + "purl": "pkg:npm/source-map-js@1.2.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + } + ] + }, + { + "type": "library", + "name": "source-map", + "version": "0.7.6", + "bom-ref": "pkg:npm/source-map@0.7.6", + "purl": "pkg:npm/source-map@0.7.6", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==" + } + ] + }, + { + "type": "library", + "name": "sucrase", + "version": "3.35.1", + "bom-ref": "pkg:npm/sucrase@3.35.1", + "purl": "pkg:npm/sucrase@3.35.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==" + } + ] + }, + { + "type": "library", + "name": "svgo", + "version": "4.0.2", + "bom-ref": "pkg:npm/svgo@4.0.2", + "purl": "pkg:npm/svgo@4.0.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "ekx94z1rRc5LDi6oSUaeRnYhd0UOJxdtQCL2rF8xpWxD3TPAsISWOrxezqGovqS38GRZOdpDfvQe3ts6F7nsng==" + } + ] + }, + { + "type": "library", + "name": "tailwindcss", + "version": "4.3.2", + "bom-ref": "pkg:npm/tailwindcss@4.3.2", + "purl": "pkg:npm/tailwindcss@4.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==" + } + ] + }, + { + "type": "library", + "name": "tapable", + "version": "2.3.3", + "bom-ref": "pkg:npm/tapable@2.3.3", + "purl": "pkg:npm/tapable@2.3.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==" + } + ] + }, + { + "type": "library", + "name": "thenify-all", + "version": "1.6.0", + "bom-ref": "pkg:npm/thenify-all@1.6.0", + "purl": "pkg:npm/thenify-all@1.6.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==" + } + ] + }, + { + "type": "library", + "name": "thenify", + "version": "3.3.1", + "bom-ref": "pkg:npm/thenify@3.3.1", + "purl": "pkg:npm/thenify@3.3.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==" + } + ] + }, + { + "type": "library", + "name": "tinyexec", + "version": "0.3.2", + "bom-ref": "pkg:npm/tinyexec@0.3.2", + "purl": "pkg:npm/tinyexec@0.3.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" + } + ] + }, + { + "type": "library", + "name": "tinyexec", + "version": "1.2.4", + "bom-ref": "pkg:npm/tinyexec@1.2.4", + "purl": "pkg:npm/tinyexec@1.2.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==" + } + ] + }, + { + "type": "library", + "name": "tinyglobby", + "version": "0.2.17", + "bom-ref": "pkg:npm/tinyglobby@0.2.17", + "purl": "pkg:npm/tinyglobby@0.2.17", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==" + } + ] + }, + { + "type": "library", + "name": "to-regex-range", + "version": "5.0.1", + "bom-ref": "pkg:npm/to-regex-range@5.0.1", + "purl": "pkg:npm/to-regex-range@5.0.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==" + } + ] + }, + { + "type": "library", + "name": "tree-kill", + "version": "1.2.2", + "bom-ref": "pkg:npm/tree-kill@1.2.2", + "purl": "pkg:npm/tree-kill@1.2.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" + } + ] + }, + { + "type": "library", + "name": "ts-api-utils", + "version": "2.5.0", + "bom-ref": "pkg:npm/ts-api-utils@2.5.0", + "purl": "pkg:npm/ts-api-utils@2.5.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==" + } + ] + }, + { + "type": "library", + "name": "ts-interface-checker", + "version": "0.1.13", + "bom-ref": "pkg:npm/ts-interface-checker@0.1.13", + "purl": "pkg:npm/ts-interface-checker@0.1.13", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + } + ] + }, + { + "type": "library", + "name": "tslib", + "version": "2.8.1", + "bom-ref": "pkg:npm/tslib@2.8.1", + "purl": "pkg:npm/tslib@2.8.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + ] + }, + { + "type": "library", + "name": "tsup", + "version": "8.5.1", + "bom-ref": "pkg:npm/tsup@8.5.1", + "purl": "pkg:npm/tsup@8.5.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==" + } + ] + }, + { + "type": "library", + "name": "type-check", + "version": "0.4.0", + "bom-ref": "pkg:npm/type-check@0.4.0", + "purl": "pkg:npm/type-check@0.4.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==" + } + ] + }, + { + "type": "library", + "name": "typescript-eslint", + "version": "8.62.1", + "bom-ref": "pkg:npm/typescript-eslint@8.62.1", + "purl": "pkg:npm/typescript-eslint@8.62.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "vymnnM5g0AKQDSAyfP12nMIBvgwgA42syg74kkuZ4x1VuTzwQKwc5h9rGxeShCjny5o+zWAb6OEoz7XLgrIkIw==" + } + ] + }, + { + "type": "library", + "name": "typescript-eslint", + "version": "8.65.0", + "bom-ref": "pkg:npm/typescript-eslint@8.65.0", + "purl": "pkg:npm/typescript-eslint@8.65.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==" + } + ] + }, + { + "type": "library", + "name": "typescript", + "version": "5.9.3", + "bom-ref": "pkg:npm/typescript@5.9.3", + "purl": "pkg:npm/typescript@5.9.3", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==" + } + ] + }, + { + "type": "library", + "name": "ufo", + "version": "1.6.4", + "bom-ref": "pkg:npm/ufo@1.6.4", + "purl": "pkg:npm/ufo@1.6.4", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==" + } + ] + }, + { + "type": "library", + "name": "undici-types", + "version": "8.3.0", + "bom-ref": "pkg:npm/undici-types@8.3.0", + "purl": "pkg:npm/undici-types@8.3.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==" + } + ] + }, + { + "type": "library", + "name": "uri-js", + "version": "4.4.1", + "bom-ref": "pkg:npm/uri-js@4.4.1", + "purl": "pkg:npm/uri-js@4.4.1", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==" + } + ] + }, + { + "type": "library", + "name": "whatwg-mimetype", + "version": "3.0.0", + "bom-ref": "pkg:npm/whatwg-mimetype@3.0.0", + "purl": "pkg:npm/whatwg-mimetype@3.0.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" + } + ] + }, + { + "type": "library", + "name": "which", + "version": "2.0.2", + "bom-ref": "pkg:npm/which@2.0.2", + "purl": "pkg:npm/which@2.0.2", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==" + } + ] + }, + { + "type": "library", + "name": "word-wrap", + "version": "1.2.5", + "bom-ref": "pkg:npm/word-wrap@1.2.5", + "purl": "pkg:npm/word-wrap@1.2.5", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==" + } + ] + }, + { + "type": "library", + "name": "ws", + "version": "8.21.0", + "bom-ref": "pkg:npm/ws@8.21.0", + "purl": "pkg:npm/ws@8.21.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==" + } + ] + }, + { + "type": "library", + "name": "yocto-queue", + "version": "0.1.0", + "bom-ref": "pkg:npm/yocto-queue@0.1.0", + "purl": "pkg:npm/yocto-queue@0.1.0", + "properties": [ + { + "name": "wrnexus:dependency-source", + "value": "bun.lock" + } + ], + "hashes": [ + { + "alg": "SHA-512", + "content": "rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + } + ] + } + ] +} diff --git a/SECURITY-PERFORMANCE-0.7.0.md b/SECURITY-PERFORMANCE-0.7.0.md new file mode 100644 index 00000000..85fea651 --- /dev/null +++ b/SECURITY-PERFORMANCE-0.7.0.md @@ -0,0 +1,3 @@ +# WRNexusJS 0.7.0 implementation summary + +The complete 21-area implementation matrix is in `docs/SECURITY-PERFORMANCE-0.7.md`. The source release includes executable validation, tests, migration, SBOM generation, benchmarks, production configuration defaults, and five new framework packages. diff --git a/UPGRADE-0.7.0.md b/UPGRADE-0.7.0.md new file mode 100644 index 00000000..52de64ef --- /dev/null +++ b/UPGRADE-0.7.0.md @@ -0,0 +1,24 @@ +# Upgrade to WRNexusJS 0.7.0 + +```sh +bunx @wrnexus/cli@0.7.0 update --version=0.7.0 --report +bun install +bun run validate:0.7 +bun run check:production +``` + +## Navigation performance change + +The updater adds `navigation.mode: "auto"` when navigation is not already configured. In auto mode, fully static pages ship no WRNexusJS JavaScript and links use native document navigation. Pages that already require browser behavior receive progressive client navigation. + +Applications that require in-place navigation on every route can preserve the previous behavior explicitly: + +```ts +navigation: { + mode: "client", +} +``` + +Use `mode: "document"` to disable client navigation everywhere. + +Review CSP sources, Trusted Types compatibility, CORS origins, trusted proxy/host settings, tenant authorization, upload scanner adapters, remote image hosts, cache privacy, WebSocket limits, and performance budgets before enforcing production mode. diff --git a/bun.lock b/bun.lock index 7376ac28..63d605c3 100644 --- a/bun.lock +++ b/bun.lock @@ -4,7 +4,6 @@ "workspaces": { "": { "name": "wrnexus", - "version": "0.6.0", "devDependencies": { "@eslint/js": "latest", "@types/bun": "latest", @@ -87,11 +86,11 @@ }, "packages/ai": { "name": "@wrnexus/ai", - "version": "0.6.0", + "version": "0.7.0", }, "packages/auth": { "name": "@wrnexus/auth", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/authz": "workspace:*", "@wrnexus/captcha": "workspace:*", @@ -113,11 +112,22 @@ }, "packages/authz": { "name": "@wrnexus/authz", - "version": "0.6.0", + "version": "0.7.0", + }, + "packages/benchmark": { + "name": "@wrnexus/benchmark", + "version": "0.7.0", + }, + "packages/cache": { + "name": "@wrnexus/cache", + "version": "0.7.0", + "dependencies": { + "@wrnexus/core": "workspace:*", + }, }, "packages/captcha": { "name": "@wrnexus/captcha", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", "@wrnexus/plugin": "workspace:*", @@ -131,7 +141,7 @@ }, "packages/cli": { "name": "@wrnexus/cli", - "version": "0.6.0", + "version": "0.7.0", "bin": { "wrnexus": "src/index.ts", }, @@ -152,29 +162,30 @@ }, "packages/compiler": { "name": "@wrnexus/compiler", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { + "@wrnexus/store": "workspace:*", "@wrnexus/syntax": "workspace:*", }, }, "packages/core": { "name": "@wrnexus/core", - "version": "0.6.0", + "version": "0.7.0", }, "packages/csr": { "name": "@wrnexus/csr", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", }, }, "packages/db": { "name": "@wrnexus/db", - "version": "0.6.0", + "version": "0.7.0", }, "packages/dev-server": { "name": "@wrnexus/dev-server", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/compiler": "workspace:*", "@wrnexus/core": "workspace:*", @@ -182,10 +193,13 @@ "@wrnexus/db": "workspace:*", "@wrnexus/dev-toolbar": "workspace:*", "@wrnexus/i18n": "workspace:*", + "@wrnexus/observability": "workspace:*", "@wrnexus/plugin": "workspace:*", "@wrnexus/pubsub": "workspace:*", "@wrnexus/router": "workspace:*", + "@wrnexus/security": "workspace:*", "@wrnexus/ssr": "workspace:*", + "@wrnexus/store": "workspace:*", "@wrnexus/styles": "workspace:*", "@wrnexus/ui": "workspace:*", "@wrnexus/uploader": "workspace:*", @@ -194,7 +208,7 @@ }, "packages/dev-toolbar": { "name": "@wrnexus/dev-toolbar", - "version": "0.6.0", + "version": "0.7.0", "devDependencies": { "@types/bun": "latest", "typescript": "^5.9.2", @@ -202,78 +216,105 @@ }, "packages/encryption": { "name": "@wrnexus/encryption", - "version": "0.6.0", + "version": "0.7.0", }, "packages/helpers": { "name": "@wrnexus/helpers", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", }, }, "packages/i18n": { "name": "@wrnexus/i18n", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", }, }, + "packages/image": { + "name": "@wrnexus/image", + "version": "0.7.0", + "dependencies": { + "@wrnexus/security": "workspace:*", + }, + }, "packages/jwt": { "name": "@wrnexus/jwt", - "version": "0.6.0", + "version": "0.7.0", }, "packages/mobile": { "name": "@wrnexus/mobile", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/native": "workspace:*", }, }, "packages/native": { "name": "@wrnexus/native", - "version": "0.6.0", + "version": "0.7.0", }, "packages/oauth": { "name": "@wrnexus/oauth", - "version": "0.6.0", + "version": "0.7.0", + }, + "packages/observability": { + "name": "@wrnexus/observability", + "version": "0.7.0", + "dependencies": { + "@wrnexus/core": "workspace:*", + }, }, "packages/plugin": { "name": "@wrnexus/plugin", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/syntax": "workspace:*", }, }, "packages/pubsub": { "name": "@wrnexus/pubsub", - "version": "0.6.0", + "version": "0.7.0", }, "packages/queue": { "name": "@wrnexus/queue", - "version": "0.6.0", + "version": "0.7.0", }, "packages/reactive": { "name": "@wrnexus/reactive", - "version": "0.6.0", + "version": "0.7.0", }, "packages/router": { "name": "@wrnexus/router", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/compiler": "workspace:*", "@wrnexus/core": "workspace:*", }, }, - "packages/ssr": { - "name": "@wrnexus/ssr", - "version": "0.6.0", + "packages/security": { + "name": "@wrnexus/security", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", }, }, + "packages/ssr": { + "name": "@wrnexus/ssr", + "version": "0.7.0", + "dependencies": { + "@wrnexus/core": "workspace:*", + "@wrnexus/security": "workspace:*", + "@wrnexus/store": "workspace:*", + }, + }, + "packages/store": { + "name": "@wrnexus/store", + "version": "0.7.0", + }, "packages/styles": { "name": "@wrnexus/styles", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", "@wrnexus/plugin": "workspace:*", @@ -282,50 +323,45 @@ }, "packages/syntax": { "name": "@wrnexus/syntax", - "version": "0.6.0", + "version": "0.7.0", }, "packages/test": { "name": "@wrnexus/test", - "version": "0.6.0", + "version": "0.7.0", }, "packages/tracking": { "name": "@wrnexus/tracking", - "version": "0.6.0", + "version": "0.7.0", + }, + "packages/typecheck": { + "name": "@wrnexus/typecheck", + "version": "0.7.0", + "dependencies": { + "@wrnexus/syntax": "workspace:*", + "typescript": "^5.5.0", + }, }, "packages/ui": { "name": "@wrnexus/ui", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", }, }, "packages/uploader": { "name": "@wrnexus/uploader", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/core": "workspace:*", }, }, "packages/validation": { "name": "@wrnexus/validation", - "version": "0.6.0", - }, - "packages/store": { - "name": "@wrnexus/store", - "version": "0.6.0", - }, - "packages/typecheck": { - "name": "@wrnexus/typecheck", - "version": "0.6.0", - "dependencies": { - "@wrnexus/compiler": "workspace:*", - "@wrnexus/syntax": "workspace:*", - "typescript": "^5.5.0", - }, + "version": "0.7.0", }, "services/managed-captcha": { "name": "@wrnexus/managed-captcha-service", - "version": "0.6.0", + "version": "0.7.0", "dependencies": { "@wrnexus/captcha": "workspace:*", }, @@ -589,6 +625,10 @@ "@wrnexus/authz": ["@wrnexus/authz@workspace:packages/authz"], + "@wrnexus/benchmark": ["@wrnexus/benchmark@workspace:packages/benchmark"], + + "@wrnexus/cache": ["@wrnexus/cache@workspace:packages/cache"], + "@wrnexus/captcha": ["@wrnexus/captcha@workspace:packages/captcha"], "@wrnexus/cli": ["@wrnexus/cli@workspace:packages/cli"], @@ -611,6 +651,8 @@ "@wrnexus/i18n": ["@wrnexus/i18n@workspace:packages/i18n"], + "@wrnexus/image": ["@wrnexus/image@workspace:packages/image"], + "@wrnexus/jwt": ["@wrnexus/jwt@workspace:packages/jwt"], "@wrnexus/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"], @@ -621,6 +663,8 @@ "@wrnexus/oauth": ["@wrnexus/oauth@workspace:packages/oauth"], + "@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"], + "@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"], "@wrnexus/pubsub": ["@wrnexus/pubsub@workspace:packages/pubsub"], @@ -631,6 +675,8 @@ "@wrnexus/router": ["@wrnexus/router@workspace:packages/router"], + "@wrnexus/security": ["@wrnexus/security@workspace:packages/security"], + "@wrnexus/ssr": ["@wrnexus/ssr@workspace:packages/ssr"], "@wrnexus/store": ["@wrnexus/store@workspace:packages/store"], diff --git a/docs/SECURITY-PERFORMANCE-0.7.md b/docs/SECURITY-PERFORMANCE-0.7.md new file mode 100644 index 00000000..647f8b3a --- /dev/null +++ b/docs/SECURITY-PERFORMANCE-0.7.md @@ -0,0 +1,151 @@ +# WRNexusJS 0.7 Security and Performance Foundation + +WRNexusJS 0.7 makes the secure and fast path the framework default. This document maps the 21 production-hardening areas to concrete packages, runtime behavior, diagnostics, and release gates. Application-specific authorization, infrastructure capacity, CSP allowlists, malware engines, Redis/CDN adapters, and deployment configuration still require project review. + +## Default production targets + +- LCP <= 2.5 s, INP <= 200 ms, CLS <= 0.1. +- Long tasks <= 50 ms. +- Preferred initial route JavaScript <= 50 KiB compressed-equivalent source budget. +- Preferred critical route CSS <= 25 KiB. +- Sensitive responses use private/no-store policies. +- Static pages are classified so builds can avoid unnecessary client runtime work. + +## 1. Framework targets and budgets + +Implemented in `@wrnexus/core/performance` and the production build report. Budgets now cover route JS, CSS, HTML, images, hydration, SSR, LCP, INP, CLS, TTFB, long tasks, hydration boundaries, and request count. Builds can warn or fail through `performance.enforcement`. + +## 2. Compiler and template security + +- Context-aware URL attributes reject dangerous protocols in SSR and reactive updates. +- Static `javascript:`, `vbscript:`, `file:`, unsafe `data:` URLs are compiler diagnostics. +- Client functions using dynamic-code or unsafe DOM HTML sinks are rejected. +- Environment-derived secrets cannot enter client state; sensitive values cannot use browser persistence. +- Server/client state and function boundaries remain compiler-enforced. +- Hydration/store payloads use bounded, HTML-safe serialization with redaction and prototype-pollution checks. + +## 3. Browser security headers + +`@wrnexus/core` provides CSP, Trusted Types, HSTS, COOP, CORP, Referrer-Policy, Permissions-Policy, X-Content-Type-Options, frame protection, Origin-Agent-Cluster, and DNS-prefetch controls. `@wrnexus/security` adds balanced, strict, and API presets. CSP is nonce-aware and can run report-only. + +## 4. Sessions and authentication + +The core session layer now supports idle and absolute expiry policies, sliding access, configurable secure cookie defaults, and server-side session backends. `@wrnexus/auth` remains responsible for password/passkey/MFA/account flows. Credentials must remain in Secure, HttpOnly cookies, not browser storage. + +## 5. CSRF, CORS, and authorization + +CSRF validates token, Origin, and Fetch Metadata for unsafe methods. CORS remains opt-in and exact-origin capable. `@wrnexus/authz` and tenant middleware provide server-side policy and tenant scope; UI hiding is never treated as authorization. + +## 6. Gateway and API hardening + +Gateway and app runtime support URL/header/query/body limits, timeouts, concurrency limits, host allowlists, Fetch Metadata, IP rate limits, trusted forwarding, access logs, request IDs, and WebSocket origin/message/queue controls. + +## 7. SSRF protection + +`@wrnexus/security/safeFetch` validates protocol/host, resolves every address, blocks loopback/private/link-local/multicast ranges, revalidates redirects, limits redirects and response size, applies timeouts, and never forwards credentials unless explicitly allowed. + +## 8. File-upload security + +Core upload primitives support extension and MIME allowlists, size/aggregate limits, random filenames, path safety, inspector and malware-scanner adapters, response hardening, and controlled download disposition. Production applications connect their antivirus/CDR engine through the scanner interface. + +## 9. Database performance and security + +`instrumentDb` adds query timeout, max-row enforcement, slow-query reporting, duplicate/N+1 detection, SELECT-star and unbounded-query warnings, and per-request query records. Existing drivers continue parameterized query handling and transaction support. + +## 10. SSR and server performance + +Existing streaming responses, request-local dedupe, route caching primitives, and static manifests remain. The compiler now classifies routes as static, static-interactive, request SSR, authenticated SSR, streaming SSR, or dynamic; the production build report records prerenderability and runtime needs. + +## 11. Hydration and CSR performance + +WRNexusJS now defaults navigation to `auto`: non-interactive static markup ships no framework JavaScript and uses native document navigation. Interactive routes receive only the runtimes their rendered markers require. Explicit `client` and `document` modes remain available. Partial hydration boundaries, lazy hydration strategies, keyed list preservation, direct reactive DOM updates, batched signal work, navigation cancellation, page-store disposal, and delegated framework event handling remain. Hydration duration emits `wrnexus:hydrated` telemetry. + +## 12. Bundle and build optimization + +Production builds are minified, content-hashed where assets are emitted, server/client separated, and measured through `build-report.json`. Package runtimes remain independently emitted and immutable. Runtime classification and budget failures are release-visible. Persistent build-cache and deployment adapters remain configurable. + +## 13. CSS performance + +Styles remain production-minified and package-aware. `auditCssPerformance` detects `transition: all`, expensive blur/shadow patterns, broad universal selectors, and duplicate keyframes while preserving the existing design-token, contrast, and Tailwind source audits. + +## 14. Images, icons, and fonts + +`@wrnexus/image` creates validated responsive image attributes, deduplicated srcsets, sizes, dimensions, loading/decoding/fetch-priority policies, secure remote-host rules, and LCP/oversizing audits. Existing font helpers retain self-hosting/preload/CSP support. UI packages continue per-icon imports rather than a whole-library browser bundle. + +## 15. HTTP delivery + +The runtime negotiates Brotli first and gzip fallback, avoids recompressing unsuitable responses, supports ETags/conditional requests, immutable versioned framework assets, cache-control helpers including stale-while-revalidate/stale-if-error, preload/modulepreload, and proxy-aware HTTPS handling. + +## 16. Realtime and WebSocket security + +Rooms now support authentication requirements, max message bytes, messages/second, room/user connection quotas, maximum JSON depth, message schema/authorization predicates, prototype-pollution rejection, violation callbacks, origin validation, and gateway queue limits. Pub/sub bridge behavior remains available for horizontal scaling. + +## 17. Observability + +`@wrnexus/observability` provides counters, gauges, histograms, HTTP middleware, request/error/active-request metrics, same-origin Web Vitals ingestion, an opt-in browser collector, and exporter adapters. It avoids collecting request bodies or user identifiers by default. + +## 18. DevToolbar + +Security checks now include secret query parameters, insecure forms, missing CSRF, CSP nonce issues, inline styles, mixed content, unsandboxed iframes, sensitive local storage, and leaked hydration fields. Performance checks cover total/JS transfer, slow resources, DOM size, hydration count, oversized images, render-blocking assets, and long tasks. + +## 19. Tests and release gates + +`validate:0.7`, `security:framework`, `benchmark:framework`, typecheck, lint, tests, formatting, UI reference verification, and package staging run before release. New package tests cover serialization, prototype pollution, URL policy, caching, images, metrics, Web Vitals, benchmarks, and realtime limits. + +## 20. Supply-chain security + +The release remains private/restricted, requires aligned versions and an explicit migration, checks for secret-like files and dangerous static URLs, generates CycloneDX `SBOM-0.7.0.cdx.json`, verifies staged package contents, requires clean/pushed Git state, and verifies registry privacy after publication. Registry MFA and short-lived publishing credentials remain operational requirements. + +## 21. New packages + +- `@wrnexus/security`: security policy, secure serialization, URL validation, request limits, secure cookies, SSRF-safe fetch. +- `@wrnexus/cache`: bounded tag cache, deduplication, response caching, ETag, stale policies and invalidation. +- `@wrnexus/image`: responsive image planning and audits. +- `@wrnexus/observability`: metrics, request instrumentation, Web Vitals, exporters. +- `@wrnexus/benchmark`: deterministic benchmarks and regression budgets. + +## Production configuration baseline + +```ts +export default { + navigation: { mode: "auto" }, + security: { + headers: true, + requestLimits: { + maxUrlLength: 8192, + maxHeaderCount: 100, + maxHeaderBytes: 32768, + maxQueryParameters: 100, + maxBodyBytes: 10485760, + timeoutMs: 30000, + maxConcurrent: 1000, + fetchMetadata: true, + }, + contentSecurityPolicy: { enabled: true, useDefaults: true }, + trustedTypes: { enabled: true, requireForScript: true }, + hsts: { enabled: true, maxAge: 63072000, includeSubDomains: true, preload: true }, + }, + performance: { + enforcement: "error", + analyze: true, + budgets: { + routeJsBytes: 51200, + routeCssBytes: 25600, + hydrationMs: 200, + serverRenderMs: 500, + lcpMs: 2500, + inpMs: 200, + cls: 0.1, + ttfbMs: 800, + }, + }, + observability: { + enabled: true, + serverTiming: true, + sampleRate: 0.1, + webVitals: true, + }, +}; +``` + +Use report-only CSP and warning-level performance enforcement while migrating an existing application, then enforce after resolving findings. diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index 07f61368..3e062417 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "wrnexus", - "version": "0.5.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wrnexus", - "version": "0.5.0", + "version": "0.7.0", "license": "SEE LICENSE IN LICENSE", "devDependencies": { "@vscode/vsce": "^3.9.2" diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 96d92dd3..17219c11 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -2,7 +2,7 @@ "name": "wrnexus", "displayName": "WRNexus Language Support", "description": "Complete WRNexus v0.6 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.", - "version": "0.6.0", + "version": "0.7.0", "publisher": "wrnexus", "private": true, "license": "SEE LICENSE IN LICENSE", diff --git a/editors/vscode/src/compiler.cjs b/editors/vscode/src/compiler.cjs index f5279ffd..9016a761 100644 --- a/editors/vscode/src/compiler.cjs +++ b/editors/vscode/src/compiler.cjs @@ -3,6 +3,79 @@ const __nodeRequire = require; const __path = __nodeRequire("node:path"); const __modules = { +"packages/compiler/src/analysis.ts": function (module, exports, require, __filename, __dirname) { +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.analyzeRuntimeRequirements = analyzeRuntimeRequirements; +function hasEvent(nodes) { + for (const node of nodes) { + if (node.type === "element") { + if (node.attrs.some((attribute) => attribute.event)) + return true; + if (hasEvent(node.children)) + return true; + } + else if (node.type === "each") { + if (hasEvent(node.body) || hasEvent(node.empty)) + return true; + } + else if (node.type === "if") { + if (node.branches.some((branch) => hasEvent(branch.body))) + return true; + } + } + return false; +} +function analyzeRuntimeRequirements(ast) { + const reasons = []; + const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server"); + const clientState = ast.states.some((state) => state.runtime !== "server"); + const interactive = clientFunctions || + clientState || + ast.effects.length > 0 || + ast.watches.length > 0 || + hasEvent(ast.view); + if (interactive) + reasons.push("client interactivity"); + const requestData = ast.loads.length > 0 || + ast.actions.length > 0 || + ast.dataApis.length > 0 || + ast.apis.length > 0 || + ast.realtimes.length > 0 || + ast.runtimeFunctions.some((fn) => fn.runtime === "server") || + ast.states.some((state) => state.runtime === "server"); + if (requestData) + reasons.push("server/request data"); + const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? ""); + if (authenticated) + reasons.push("authentication required"); + const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? ""); + if (streaming) + reasons.push("streaming enabled"); + let kind; + if (streaming) + kind = "streaming-ssr"; + else if (authenticated) + kind = "authenticated-ssr"; + else if (requestData && interactive) + kind = "dynamic"; + else if (requestData) + kind = "request-ssr"; + else if (interactive) + kind = "static-interactive"; + else + kind = "static"; + return { + kind, + canPrerender: kind === "static" || kind === "static-interactive", + needsClientRuntime: interactive && ast.hydrate !== "none" && ast.runtime !== "server", + needsServerRuntime: requestData || authenticated || streaming || ast.runtime === "server", + hydrationStrategy: interactive ? (ast.hydrate ?? "load") : null, + reasons, + }; +} + +}, "packages/compiler/src/cache.ts": function (module, exports, require, __filename, __dirname) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); @@ -339,6 +412,30 @@ const HTML_BOOLEAN_ATTRIBUTES = new Set([ function isHtmlBooleanAttribute(name) { return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase()); } +const URL_ATTRIBUTES = new Set([ + "href", + "src", + "action", + "formaction", + "poster", + "cite", + "background", + "xlink:href", +]); +function sanitizeUrlAttribute(value) { + const compact = value.trim().replace(/[\u0000-\u0020]+/g, ""); + const lower = compact.toLowerCase(); + if (/^(?:javascript|vbscript|file):/.test(lower)) + return "about:blank"; + if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(lower)) + return "about:blank"; + return value; +} +function safeAttributeValue(name, value) { + if (!URL_ATTRIBUTES.has(name.toLowerCase()) || value.includes("{")) + return value; + return sanitizeUrlAttribute(value); +} /** Escape a value placed inside a double-quoted HTML attribute. */ function attrEscape(value) { return value @@ -368,7 +465,9 @@ function renderAttr(attr) { case "csrText": return ""; default: - return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(attr.value)}"`; + return attr.boolean + ? ` ${attr.name}` + : ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`; } } function eventAttribute(name) { @@ -423,7 +522,7 @@ function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) { if (initial === null) return base; const marker = JSON.stringify([attr.name, attr.value]); - return ` ${attr.name}="${attrEscape(initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`; + return ` ${attr.name}="${attrEscape(URL_ATTRIBUTES.has(attr.name.toLowerCase()) ? sanitizeUrlAttribute(initial) : initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`; }) .join(""); return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered; @@ -2009,7 +2108,7 @@ function renderPageComponentAttr(attr, dynamicExpressions) { } const expression = wholeAttributeExpression(attr.value); if (!expression) { - return ` ${attr.name}="${attrEscape(attr.value)}"`; + return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`; } dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; @@ -2097,7 +2196,7 @@ function resolveWrnImports(declarations, importer, options) { * `@wrnexus/syntax` package. This package owns platform-specific codegen. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.DependencyGraph = exports.createCompilationCache = exports.compilationKey = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.createWrnSourceMap = exports.resolveWrnImports = exports.resolveWrnImport = exports.createComponentContract = exports.generateStoreModule = exports.generateStoreBrowserModule = exports.generateDeclarations = exports.rpcManifest = exports.generateServerFunctionsModule = exports.generateBrowserModule = exports.generateTargets = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = void 0; +exports.DependencyGraph = exports.createCompilationCache = exports.compilationKey = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.LexError = exports.Lexer = exports.NativeCompileError = exports.generateNative = exports.analyzeRuntimeRequirements = exports.createWrnSourceMap = exports.resolveWrnImports = exports.resolveWrnImport = exports.createComponentContract = exports.generateStoreModule = exports.generateStoreBrowserModule = exports.generateDeclarations = exports.rpcManifest = exports.generateServerFunctionsModule = exports.generateBrowserModule = exports.generateTargets = exports.generate = exports.ParseError = exports.parse = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.assertValidAst = void 0; exports.compileNativeWireFile = compileNativeWireFile; exports.compileWireFile = compileWireFile; exports.compile = compile; @@ -2132,6 +2231,8 @@ Object.defineProperty(exports, "resolveWrnImport", { enumerable: true, get: func Object.defineProperty(exports, "resolveWrnImports", { enumerable: true, get: function () { return import_resolver_ts_1.resolveWrnImports; } }); var source_map_ts_1 = require("./source-map.js"); Object.defineProperty(exports, "createWrnSourceMap", { enumerable: true, get: function () { return source_map_ts_1.createWrnSourceMap; } }); +var analysis_ts_1 = require("./analysis.js"); +Object.defineProperty(exports, "analyzeRuntimeRequirements", { enumerable: true, get: function () { return analysis_ts_1.analyzeRuntimeRequirements; } }); var native_codegen_ts_2 = require("./native-codegen.js"); Object.defineProperty(exports, "generateNative", { enumerable: true, get: function () { return native_codegen_ts_2.generateNative; } }); Object.defineProperty(exports, "NativeCompileError", { enumerable: true, get: function () { return native_codegen_ts_2.NativeCompileError; } }); @@ -2649,7 +2750,7 @@ function __diagnostic(code, message, details) { } function __csrfToken() { if (typeof document === "undefined") return undefined; - const match = /(?:^|;\\s*)wrnexus_csrf=([^;]+)/.exec(document.cookie || ""); + const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || ""); return match ? decodeURIComponent(match[1]) : undefined; } async function __callServerFunction(storeName, functionName, args, options) { @@ -2660,7 +2761,7 @@ async function __callServerFunction(storeName, functionName, args, options) { method: "POST", credentials: "same-origin", signal: options.signal, - headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-wrnexus-csrf": csrf } : {}, options.headers || {}), + headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}), body: JSON.stringify({ component: storeName, function: functionName, args: args }), }); const payload = await response.json().catch(function () { return null; }); @@ -3129,20 +3230,65 @@ function astDiagnostics(ast, options) { }); } let interactive = ast.states.length > 0 || ast.effects.length > 0 || ast.watches.length > 0; + const urlAttributes = new Set(["href", "src", "action", "formaction", "poster", "cite", "background", "xlink:href"]); walk(ast.view, (node) => { - if (node.type === "element" && node.attrs.some((attribute) => attribute.event)) - interactive = true; - if (!options.accessibility || node.type !== "element") + if (node.type !== "element") return; + if (node.attrs.some((attribute) => attribute.event)) + interactive = true; const tag = node.tag.toLowerCase(); - if (tag === "img" && !node.attrs.some((attribute) => attribute.name === "alt")) { - diagnostics.push({ - code: spec_ts_1.WRN_DIAGNOSTIC_CODES.accessibility, - severity: "warning", - message: "Image is missing an alt attribute.", - hint: 'Add alt text, or alt="" for a decorative image.', - file: options.file, - }); + for (const attribute of node.attrs) { + if (attribute.event || attribute.boolean || !urlAttributes.has(attribute.name.toLowerCase())) + continue; + if (attribute.value.includes("{")) + continue; + const value = attribute.value.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase(); + if (/^(?:javascript|vbscript|file):/.test(value) || /^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(value)) { + diagnostics.push({ + code: "WRN-SEC-UNSAFE-URL", + severity: "error", + message: `Unsafe URL protocol in ${attribute.name} on <${node.tag}>.`, + hint: "Use a relative URL, https:, mailto:, tel:, or a framework-validated URL helper.", + file: options.file, + }); + } + } + if (tag === "a") { + const target = node.attrs.find((attribute) => attribute.name === "target")?.value; + const rel = node.attrs.find((attribute) => attribute.name === "rel")?.value ?? ""; + if (target === "_blank" && !/\bnoopener\b/i.test(rel)) { + diagnostics.push({ + code: "WRN-SEC-BLANK-REL", + severity: "warning", + message: "A target=_blank link should include rel=noopener.", + hint: 'Add rel="noopener noreferrer".', + file: options.file, + }); + } + } + if (!options.accessibility) + return; + if (tag === "img") { + if (!node.attrs.some((attribute) => attribute.name === "alt")) { + diagnostics.push({ + code: spec_ts_1.WRN_DIAGNOSTIC_CODES.accessibility, + severity: "warning", + message: "Image is missing an alt attribute.", + hint: 'Add alt text, or alt="" for a decorative image.', + file: options.file, + }); + } + const hasWidth = node.attrs.some((attribute) => attribute.name === "width"); + const hasHeight = node.attrs.some((attribute) => attribute.name === "height"); + if (!hasWidth || !hasHeight) { + diagnostics.push({ + code: "WRN-PERF-IMAGE-DIMENSIONS", + severity: "warning", + message: "Image width and height are required to prevent layout shifts.", + hint: "Declare intrinsic width and height, or use @wrnexus/image.", + file: options.file, + }); + } } }); if (ast.runtime === "server" && interactive) { @@ -3171,6 +3317,24 @@ function astDiagnostics(ast, options) { if (fn.runtime === "server" && /\b(?:window|document|localStorage|sessionStorage|navigator)\b/.test(fn.body)) { diagnostics.push({ code: "WRN-SERVER-BROWSER-API", severity: "error", message: `Server function '${fn.name}' references a browser-only API.`, hint: "Move that code into a client function.", file: options.file }); } + if (fn.runtime !== "server" && /\b(?:eval\s*\(|new\s+Function\s*\(|document\.write\s*\(|\.innerHTML\s*=|\.outerHTML\s*=|insertAdjacentHTML\s*\()/.test(fn.body)) { + diagnostics.push({ + code: "WRN-SEC-DOM-SINK", + severity: "error", + message: `Client function '${fn.name}' uses an unsafe dynamic-code or HTML sink.`, + hint: "Use compiled templates, textContent, typed outputs, or a reviewed TrustedHTML sanitizer.", + file: options.file, + }); + } + if (fn.runtime !== "server" && /\b(?:setTimeout|setInterval)\s*\(\s*["'`]/.test(fn.body)) { + diagnostics.push({ + code: "WRN-SEC-STRING-TIMER", + severity: "error", + message: `Client function '${fn.name}' passes a string to a timer.`, + hint: "Pass a function instead of executable text.", + file: options.file, + }); + } const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name)); for (const prop of ast.props) { if (containsReadonlyPropMutation(fn.body, prop.name, parameterNames)) { @@ -3182,6 +3346,16 @@ function astDiagnostics(ast, options) { if (state.runtime === "shared" && /^(?:new\s+(?:Map|Set|WeakMap|WeakSet)|(?:async\s+)?function\b|.*=>)/.test(state.expr.trim())) { diagnostics.push({ code: "WRN-STATE-NON-SERIALIZABLE", severity: "error", message: `Shared state '${state.name}' is not safely serializable.`, hint: "Use JSON-compatible data or move the value into client/server state.", file: options.file }); } + if (state.runtime !== "server" && + /\b(?:process\.env|Bun\.env|Deno\.env|ctx\.env|import\.meta\.env)\b/.test(state.expr)) { + diagnostics.push({ + code: "WRN-SEC-SERVER-SECRET-SOURCE", + severity: "error", + message: `Browser-visible state '${state.name}' reads from a server environment source.`, + hint: "Move environment-backed values into server state and return only an explicitly safe result.", + file: options.file, + }); + } } if (ast.persist) { const stateNames = new Set(ast.states.filter((state) => state.runtime !== "server").map((state) => state.name)); diff --git a/eslint.config.js b/eslint.config.js index 2988b615..1901f6b7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -18,6 +18,10 @@ export default tseslint.config( "bun.lockb", "**/*.gen.ts", "editors/vscode/src/compiler.cjs", + "focus-shims.d.ts", + "**/focus-shims.d.ts", + "tsconfig.focus.json", + "**/tsconfig.focus.json", ], }, { diff --git a/examples/basic-app/.env b/examples/basic-app/.env.example similarity index 100% rename from examples/basic-app/.env rename to examples/basic-app/.env.example diff --git a/examples/basic-app/.env.uat b/examples/basic-app/.env.uat.example similarity index 100% rename from examples/basic-app/.env.uat rename to examples/basic-app/.env.uat.example diff --git a/examples/basic-app/ENVIRONMENT.md b/examples/basic-app/ENVIRONMENT.md new file mode 100644 index 00000000..c1b71826 --- /dev/null +++ b/examples/basic-app/ENVIRONMENT.md @@ -0,0 +1,5 @@ +# Example environment files + +Copy `.env.example` to `.env` for local development. Copy `.env.uat.example` to `.env.uat` only when testing the UAT environment. + +Never commit populated `.env` files. Keep secrets in the deployment environment or an approved secret manager. diff --git a/examples/component-showcase/app/pages/advanced-forms.wrn b/examples/component-showcase/app/pages/advanced-forms.wrn index b3a091e0..5aaccd1b 100644 --- a/examples/component-showcase/app/pages/advanced-forms.wrn +++ b/examples/component-showcase/app/pages/advanced-forms.wrn @@ -16,7 +16,7 @@ page AdvancedFormsShowcase {
-
+
Advanced Forms

Advanced Select

Theme-aware, responsive advanced select component.

@@ -26,7 +26,7 @@ page AdvancedFormsShowcase {
-
+
Advanced Forms

Combo Box

Editable autocomplete combobox with local and remote suggestions.

diff --git a/examples/component-showcase/app/pages/components/advanced-select.wrn b/examples/component-showcase/app/pages/components/advanced-select.wrn index e3d4da14..b18c6d6c 100644 --- a/examples/component-showcase/app/pages/components/advanced-select.wrn +++ b/examples/component-showcase/app/pages/components/advanced-select.wrn @@ -7,7 +7,7 @@ page AdvancedSelectDetail { state playground_name = ctx.url.searchParams.get("pg_name") ?? "advanced-select-1" state playground_value = ctx.url.searchParams.get("pg_value") ?? "design" state playground_values = JSON.parse(ctx.url.searchParams.get("pg_values") ?? "[]") - state playground_options = JSON.parse(ctx.url.searchParams.get("pg_options") ?? "[{\"value\":\"design\",\"label\":\"Design\",\"description\":\"Product and visual design\",\"icon\":\"icon-[lucide--palette]\",\"color\":\"#8b5cf6\",\"avatar\":\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E\"},{\"value\":\"engineering\",\"label\":\"Engineering\",\"description\":\"Platform and application engineering\",\"icon\":\"icon-[lucide--code-2]\",\"color\":\"#0ea5e9\",\"avatar\":\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E\"},{\"value\":\"growth\",\"label\":\"Growth\",\"description\":\"Marketing and customer growth\",\"icon\":\"icon-[lucide--trending-up]\",\"color\":\"#10b981\",\"avatar\":\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E\"},{\"value\":\"support\",\"label\":\"Support\",\"description\":\"Customer operations\",\"disabled\":false}]") + state playground_options = JSON.parse(ctx.url.searchParams.get("pg_options") ?? "[{\"value\":\"design\",\"label\":\"Design\",\"description\":\"Product and visual design\",\"icon\":\"icon-[lucide--palette]\",\"color\":\"#8b5cf6\",\"avatar\":\"/showcase-images/avatar-design.svg\"},{\"value\":\"engineering\",\"label\":\"Engineering\",\"description\":\"Platform and application engineering\",\"icon\":\"icon-[lucide--code-2]\",\"color\":\"#0ea5e9\",\"avatar\":\"/showcase-images/avatar-engineering.svg\"},{\"value\":\"growth\",\"label\":\"Growth\",\"description\":\"Marketing and customer growth\",\"icon\":\"icon-[lucide--trending-up]\",\"color\":\"#10b981\",\"avatar\":\"/showcase-images/avatar-growth.svg\"},{\"value\":\"support\",\"label\":\"Support\",\"description\":\"Customer operations\",\"disabled\":false}]") state playground_groups = JSON.parse(ctx.url.searchParams.get("pg_groups") ?? "[]") state playground_placeholder = ctx.url.searchParams.get("pg_placeholder") ?? "Choose a team" state playground_placeholderIcon = ctx.url.searchParams.get("pg_placeholderIcon") ?? "" @@ -89,7 +89,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -97,7 +97,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -105,7 +105,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -131,7 +131,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -139,7 +139,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -147,7 +147,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -171,7 +171,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -187,7 +187,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -195,7 +195,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -203,7 +203,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -228,7 +228,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -248,7 +248,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -256,7 +256,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" } ] }, @@ -269,7 +269,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -296,7 +296,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -312,7 +312,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -320,7 +320,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -328,7 +328,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -355,7 +355,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -370,7 +370,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -378,7 +378,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -386,7 +386,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -414,7 +414,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -430,7 +430,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -438,7 +438,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -446,7 +446,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -471,7 +471,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -489,7 +489,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -497,7 +497,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -505,7 +505,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -532,7 +532,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -554,7 +554,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -562,7 +562,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" } ] }, @@ -575,7 +575,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -603,7 +603,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -621,7 +621,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -629,7 +629,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -637,7 +637,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -665,7 +665,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -683,7 +683,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -691,7 +691,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -699,7 +699,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -727,7 +727,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -745,7 +745,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -753,7 +753,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -761,7 +761,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -790,7 +790,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -806,7 +806,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -814,7 +814,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -822,7 +822,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -850,7 +850,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -866,7 +866,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -874,7 +874,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -882,7 +882,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -907,7 +907,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -923,7 +923,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -931,7 +931,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -939,7 +939,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -965,7 +965,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -981,7 +981,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -989,7 +989,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -997,7 +997,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1023,7 +1023,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1039,7 +1039,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1047,7 +1047,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1055,7 +1055,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1081,7 +1081,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1097,7 +1097,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1105,7 +1105,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1113,7 +1113,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1139,7 +1139,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1155,7 +1155,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1163,7 +1163,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1171,7 +1171,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1196,7 +1196,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1212,7 +1212,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1220,7 +1220,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1228,7 +1228,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1255,7 +1255,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1271,7 +1271,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1279,7 +1279,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1287,7 +1287,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1315,7 +1315,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1331,7 +1331,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1339,7 +1339,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1347,7 +1347,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1374,7 +1374,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1390,7 +1390,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1398,7 +1398,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1406,7 +1406,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1433,7 +1433,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1449,7 +1449,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1457,7 +1457,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1465,7 +1465,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1492,7 +1492,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1509,7 +1509,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1517,7 +1517,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1525,7 +1525,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1551,7 +1551,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1567,7 +1567,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1575,7 +1575,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1583,7 +1583,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1610,7 +1610,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1626,7 +1626,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1634,7 +1634,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1642,7 +1642,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1668,7 +1668,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1684,7 +1684,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1692,7 +1692,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1700,7 +1700,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1727,7 +1727,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1743,7 +1743,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1751,7 +1751,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1759,7 +1759,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1786,7 +1786,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1802,7 +1802,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1810,7 +1810,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1818,7 +1818,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1844,7 +1844,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1862,7 +1862,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1870,7 +1870,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1878,7 +1878,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1905,7 +1905,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1921,7 +1921,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1929,7 +1929,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1937,7 +1937,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1963,7 +1963,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -1981,7 +1981,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1989,7 +1989,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1997,7 +1997,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2024,7 +2024,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2040,7 +2040,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2048,7 +2048,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2056,7 +2056,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2084,7 +2084,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2102,7 +2102,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2110,7 +2110,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2118,7 +2118,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2147,7 +2147,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2165,7 +2165,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2173,7 +2173,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2181,7 +2181,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2211,7 +2211,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2227,7 +2227,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2235,7 +2235,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2243,7 +2243,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2272,7 +2272,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2288,7 +2288,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2296,7 +2296,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2304,7 +2304,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2334,7 +2334,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2350,7 +2350,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2358,7 +2358,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2366,7 +2366,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2395,7 +2395,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2411,7 +2411,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2419,7 +2419,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2427,7 +2427,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2453,7 +2453,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2469,7 +2469,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2477,7 +2477,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2485,7 +2485,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2514,7 +2514,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2530,7 +2530,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2538,7 +2538,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2546,7 +2546,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -2573,7 +2573,7 @@ page AdvancedSelectDetail {
Live preview
-
+
Component usage.wrn
@@ -2589,7 +2589,7 @@ page AdvancedSelectDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -2597,7 +2597,7 @@ page AdvancedSelectDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -2605,7 +2605,7 @@ page AdvancedSelectDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", diff --git a/examples/component-showcase/app/pages/components/combo-box.wrn b/examples/component-showcase/app/pages/components/combo-box.wrn index 2f378a0e..9b48ed76 100644 --- a/examples/component-showcase/app/pages/components/combo-box.wrn +++ b/examples/component-showcase/app/pages/components/combo-box.wrn @@ -6,7 +6,7 @@ page ComboBoxDetail { state playground_label = ctx.url.searchParams.get("pg_label") ?? "Default autocomplete" state playground_name = ctx.url.searchParams.get("pg_name") ?? "combobox-1" state playground_value = ctx.url.searchParams.get("pg_value") ?? "" - state playground_options = JSON.parse(ctx.url.searchParams.get("pg_options") ?? "[{\"value\":\"design\",\"label\":\"Design\",\"description\":\"Product and visual design\",\"icon\":\"icon-[lucide--palette]\",\"color\":\"#8b5cf6\",\"avatar\":\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E\"},{\"value\":\"engineering\",\"label\":\"Engineering\",\"description\":\"Platform and application engineering\",\"icon\":\"icon-[lucide--code-2]\",\"color\":\"#0ea5e9\",\"avatar\":\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E\"},{\"value\":\"growth\",\"label\":\"Growth\",\"description\":\"Marketing and customer growth\",\"icon\":\"icon-[lucide--trending-up]\",\"color\":\"#10b981\",\"avatar\":\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E\"},{\"value\":\"support\",\"label\":\"Support\",\"description\":\"Customer operations\",\"icon\":\"icon-[lucide--life-buoy]\",\"color\":\"#f59e0b\"}]") + state playground_options = JSON.parse(ctx.url.searchParams.get("pg_options") ?? "[{\"value\":\"design\",\"label\":\"Design\",\"description\":\"Product and visual design\",\"icon\":\"icon-[lucide--palette]\",\"color\":\"#8b5cf6\",\"avatar\":\"/showcase-images/avatar-design.svg\"},{\"value\":\"engineering\",\"label\":\"Engineering\",\"description\":\"Platform and application engineering\",\"icon\":\"icon-[lucide--code-2]\",\"color\":\"#0ea5e9\",\"avatar\":\"/showcase-images/avatar-engineering.svg\"},{\"value\":\"growth\",\"label\":\"Growth\",\"description\":\"Marketing and customer growth\",\"icon\":\"icon-[lucide--trending-up]\",\"color\":\"#10b981\",\"avatar\":\"/showcase-images/avatar-growth.svg\"},{\"value\":\"support\",\"label\":\"Support\",\"description\":\"Customer operations\",\"icon\":\"icon-[lucide--life-buoy]\",\"color\":\"#f59e0b\"}]") state playground_groups = JSON.parse(ctx.url.searchParams.get("pg_groups") ?? "[]") state playground_placeholder = ctx.url.searchParams.get("pg_placeholder") ?? "Search teams" state playground_searchPlaceholder = ctx.url.searchParams.get("pg_searchPlaceholder") ?? "Start typing…" @@ -77,7 +77,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -85,7 +85,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -93,7 +93,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -119,7 +119,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -127,7 +127,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -135,7 +135,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -160,7 +160,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -174,7 +174,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -182,7 +182,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -190,7 +190,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -215,7 +215,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -229,7 +229,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -237,7 +237,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -245,7 +245,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -270,7 +270,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -288,7 +288,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -296,7 +296,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" } ] }, @@ -309,7 +309,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -335,7 +335,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -349,7 +349,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -357,7 +357,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -365,7 +365,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -391,7 +391,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -405,7 +405,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -413,7 +413,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -421,7 +421,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -447,7 +447,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -461,7 +461,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -469,7 +469,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -477,7 +477,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -503,7 +503,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -517,7 +517,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -525,7 +525,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -533,7 +533,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -558,7 +558,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -572,7 +572,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -580,7 +580,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -588,7 +588,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -614,7 +614,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -628,7 +628,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -636,7 +636,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -644,7 +644,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -670,7 +670,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -684,7 +684,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -692,7 +692,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -700,7 +700,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -726,7 +726,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -740,7 +740,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -748,7 +748,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -756,7 +756,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -781,7 +781,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -795,7 +795,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -803,7 +803,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -811,7 +811,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -837,7 +837,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -852,7 +852,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -860,7 +860,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -868,7 +868,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -893,7 +893,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -907,7 +907,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -915,7 +915,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -923,7 +923,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -949,7 +949,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -963,7 +963,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -971,7 +971,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -979,7 +979,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1005,7 +1005,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -1019,7 +1019,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1027,7 +1027,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1035,7 +1035,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1063,7 +1063,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -1077,7 +1077,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1085,7 +1085,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1093,7 +1093,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1121,7 +1121,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -1135,7 +1135,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1143,7 +1143,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1151,7 +1151,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1179,7 +1179,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -1193,7 +1193,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1201,7 +1201,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1209,7 +1209,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1239,7 +1239,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -1253,7 +1253,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1261,7 +1261,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1269,7 +1269,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", @@ -1295,7 +1295,7 @@ page ComboBoxDetail {
Live preview
-
+
Component usage.wrn
@@ -1309,7 +1309,7 @@ page ComboBoxDetail { "description": "Product and visual design", "icon": "icon-[lucide--palette]", "color": "#8b5cf6", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%238b5cf6\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3ED%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-design.svg" }, { "value": "engineering", @@ -1317,7 +1317,7 @@ page ComboBoxDetail { "description": "Platform and application engineering", "icon": "icon-[lucide--code-2]", "color": "#0ea5e9", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%230ea5e9\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EE%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-engineering.svg" }, { "value": "growth", @@ -1325,7 +1325,7 @@ page ComboBoxDetail { "description": "Marketing and customer growth", "icon": "icon-[lucide--trending-up]", "color": "#10b981", - "avatar": "data:image/svg+xml,%3Csvg xmlns=\u0027http://www.w3.org/2000/svg\u0027 viewBox=\u00270 0 64 64\u0027%3E%3Crect width=\u002764\u0027 height=\u002764\u0027 rx=\u002732\u0027 fill=\u0027%2310b981\u0027/%3E%3Ctext x=\u002732\u0027 y=\u002740\u0027 text-anchor=\u0027middle\u0027 font-family=\u0027Arial\u0027 font-size=\u002724\u0027 fill=\u0027white\u0027%3EG%3C/text%3E%3C/svg%3E" + "avatar": "/showcase-images/avatar-growth.svg" }, { "value": "support", diff --git a/examples/component-showcase/app/pages/components/feature-card.wrn b/examples/component-showcase/app/pages/components/feature-card.wrn index 203c1f1b..b49aaae5 100644 --- a/examples/component-showcase/app/pages/components/feature-card.wrn +++ b/examples/component-showcase/app/pages/components/feature-card.wrn @@ -127,7 +127,7 @@ page FeatureCardDetail {
Live preview
-
+
Component usage.wrn
@@ -135,7 +135,7 @@ page FeatureCardDetail { icon="icon-[lucide--arrow-right]" title="Unified citizen portal" description="Access trusted services from one responsive interface." - image="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Crect width='800' height='450' fill='%231e1b4b'/%3E%3Ccircle cx='610' cy='130' r='120' fill='%237c3aed' opacity='.55'/%3E%3Crect x='90' y='90' width='430' height='270' rx='28' fill='%23ffffff' opacity='.12'/%3E%3C/svg%3E" + image="/showcase-images/dashboard-preview.svg" imageAlt="Abstract dashboard preview" href="#portal" actionLabel="Open portal" diff --git a/examples/component-showcase/app/pages/components/image.wrn b/examples/component-showcase/app/pages/components/image.wrn index 4c93d3a9..5729bd00 100644 --- a/examples/component-showcase/app/pages/components/image.wrn +++ b/examples/component-showcase/app/pages/components/image.wrn @@ -58,12 +58,12 @@ page ImageDetail {
Live preview
-
Abstract violet circle on a dark surface
+
Abstract violet circle on a dark surface
Component usage.wrn
<Image
-  src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Crect width='800' height='450' fill='%231e1b4b'/%3E%3Ccircle cx='400' cy='225' r='120' fill='%237c3aed'/%3E%3C/svg%3E"
+  src="/showcase-images/violet-circle.svg"
   alt="Abstract violet circle on a dark surface"
   width="800"
   height="450"
@@ -80,13 +80,13 @@ page ImageDetail {
           
Live preview
-
Abstract blue thumbnail
+
Abstract blue thumbnail
Component usage.wrn
<Image
   size="sm"
-  src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Crect width='400' height='400' fill='%230284c7'/%3E%3Cpath d='M80 290 190 120l130 170z' fill='white' opacity='.7'/%3E%3C/svg%3E"
+  src="/showcase-images/blue-mountain.svg"
   alt="Abstract blue thumbnail"
   width="400"
   height="400"
@@ -104,13 +104,13 @@ page ImageDetail {
           
Live preview
-
Abstract green hero visual
+
Abstract green hero visual
Component usage.wrn
<Image
   size="lg"
-  src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600'%3E%3Crect width='1200' height='600' fill='%23059669'/%3E%3Crect x='150' y='120' width='900' height='360' rx='36' fill='white' opacity='.18'/%3E%3C/svg%3E"
+  src="/showcase-images/green-hero.svg"
   alt="Abstract green hero visual"
   width="1200"
   height="600"
diff --git a/examples/component-showcase/app/pages/layout.wrn b/examples/component-showcase/app/pages/layout.wrn
index f283e6e7..613148af 100644
--- a/examples/component-showcase/app/pages/layout.wrn
+++ b/examples/component-showcase/app/pages/layout.wrn
@@ -86,7 +86,7 @@ page LayoutShowcase {
       
-
Abstract violet circle on a dark surface
+
Abstract violet circle on a dark surface
Layout

Image

Render a responsive image with explicit dimensions, loading behavior, alternative text, sizing, and rounded treatment.

diff --git a/examples/component-showcase/public/showcase-images/avatar-design.svg b/examples/component-showcase/public/showcase-images/avatar-design.svg new file mode 100644 index 00000000..ede5fa56 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/avatar-design.svg @@ -0,0 +1 @@ +D diff --git a/examples/component-showcase/public/showcase-images/avatar-engineering.svg b/examples/component-showcase/public/showcase-images/avatar-engineering.svg new file mode 100644 index 00000000..b9667010 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/avatar-engineering.svg @@ -0,0 +1 @@ +E diff --git a/examples/component-showcase/public/showcase-images/avatar-growth.svg b/examples/component-showcase/public/showcase-images/avatar-growth.svg new file mode 100644 index 00000000..3a0087ae --- /dev/null +++ b/examples/component-showcase/public/showcase-images/avatar-growth.svg @@ -0,0 +1 @@ +G diff --git a/examples/component-showcase/public/showcase-images/blue-mountain.svg b/examples/component-showcase/public/showcase-images/blue-mountain.svg new file mode 100644 index 00000000..59fba63e --- /dev/null +++ b/examples/component-showcase/public/showcase-images/blue-mountain.svg @@ -0,0 +1 @@ + diff --git a/examples/component-showcase/public/showcase-images/dashboard-preview.svg b/examples/component-showcase/public/showcase-images/dashboard-preview.svg new file mode 100644 index 00000000..b7ac88cd --- /dev/null +++ b/examples/component-showcase/public/showcase-images/dashboard-preview.svg @@ -0,0 +1 @@ + diff --git a/examples/component-showcase/public/showcase-images/green-hero.svg b/examples/component-showcase/public/showcase-images/green-hero.svg new file mode 100644 index 00000000..96d07187 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/green-hero.svg @@ -0,0 +1 @@ + diff --git a/examples/component-showcase/public/showcase-images/logo-0-0.svg b/examples/component-showcase/public/showcase-images/logo-0-0.svg new file mode 100644 index 00000000..84eff8b8 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-0-0.svg @@ -0,0 +1 @@ +Acme diff --git a/examples/component-showcase/public/showcase-images/logo-0-1.svg b/examples/component-showcase/public/showcase-images/logo-0-1.svg new file mode 100644 index 00000000..9b7c6f0d --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-0-1.svg @@ -0,0 +1 @@ +Northstar diff --git a/examples/component-showcase/public/showcase-images/logo-0-2.svg b/examples/component-showcase/public/showcase-images/logo-0-2.svg new file mode 100644 index 00000000..782a2921 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-0-2.svg @@ -0,0 +1 @@ +Vertex diff --git a/examples/component-showcase/public/showcase-images/logo-0-3.svg b/examples/component-showcase/public/showcase-images/logo-0-3.svg new file mode 100644 index 00000000..0dd8735b --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-0-3.svg @@ -0,0 +1 @@ +Orbit diff --git a/examples/component-showcase/public/showcase-images/logo-1-0.svg b/examples/component-showcase/public/showcase-images/logo-1-0.svg new file mode 100644 index 00000000..b6d72540 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-1-0.svg @@ -0,0 +1 @@ +Acme diff --git a/examples/component-showcase/public/showcase-images/logo-1-1.svg b/examples/component-showcase/public/showcase-images/logo-1-1.svg new file mode 100644 index 00000000..ef658934 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-1-1.svg @@ -0,0 +1 @@ +Northstar diff --git a/examples/component-showcase/public/showcase-images/logo-1-2.svg b/examples/component-showcase/public/showcase-images/logo-1-2.svg new file mode 100644 index 00000000..ebfd5d41 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-1-2.svg @@ -0,0 +1 @@ +Vertex diff --git a/examples/component-showcase/public/showcase-images/logo-1-3.svg b/examples/component-showcase/public/showcase-images/logo-1-3.svg new file mode 100644 index 00000000..611bf840 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-1-3.svg @@ -0,0 +1 @@ +Orbit diff --git a/examples/component-showcase/public/showcase-images/logo-2-0.svg b/examples/component-showcase/public/showcase-images/logo-2-0.svg new file mode 100644 index 00000000..a35a4cc1 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-2-0.svg @@ -0,0 +1 @@ +Acme diff --git a/examples/component-showcase/public/showcase-images/logo-2-1.svg b/examples/component-showcase/public/showcase-images/logo-2-1.svg new file mode 100644 index 00000000..70b683d0 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-2-1.svg @@ -0,0 +1 @@ +Northstar diff --git a/examples/component-showcase/public/showcase-images/logo-2-2.svg b/examples/component-showcase/public/showcase-images/logo-2-2.svg new file mode 100644 index 00000000..1022b571 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-2-2.svg @@ -0,0 +1 @@ +Vertex diff --git a/examples/component-showcase/public/showcase-images/logo-2-3.svg b/examples/component-showcase/public/showcase-images/logo-2-3.svg new file mode 100644 index 00000000..b955fd69 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/logo-2-3.svg @@ -0,0 +1 @@ +Orbit diff --git a/examples/component-showcase/public/showcase-images/violet-circle.svg b/examples/component-showcase/public/showcase-images/violet-circle.svg new file mode 100644 index 00000000..2ef01af4 --- /dev/null +++ b/examples/component-showcase/public/showcase-images/violet-circle.svg @@ -0,0 +1 @@ + diff --git a/examples/component-showcase/scripts/generate-showcase.mjs b/examples/component-showcase/scripts/generate-showcase.mjs index 841b09b3..00d17214 100644 --- a/examples/component-showcase/scripts/generate-showcase.mjs +++ b/examples/component-showcase/scripts/generate-showcase.mjs @@ -166,7 +166,7 @@ function sampleArray(prop, component, variation) { alt: `${logo} logo`, href: `#${slugOf(component.name)}-demo`, external: false, - src: `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 120 36'%3E%3Crect width='120' height='36' rx='8' fill='%23${variation === 2 ? "059669" : variation === 1 ? "0284c7" : "7c3aed"}'/%3E%3Ctext x='60' y='23' text-anchor='middle' font-family='Arial' font-size='13' fill='white'%3E${logo}%3C/text%3E%3C/svg%3E`, + src: `/showcase-images/logo-${variation}-${index}.svg`, order: index, })); if (name.includes("navigationcolumns")) @@ -871,8 +871,7 @@ function advancedSelectConfiguration(variation) { description: "Product and visual design", icon: "icon-[lucide--palette]", color: "#8b5cf6", - avatar: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E", + avatar: "/showcase-images/avatar-design.svg", }, { value: "engineering", @@ -880,8 +879,7 @@ function advancedSelectConfiguration(variation) { description: "Platform and application engineering", icon: "icon-[lucide--code-2]", color: "#0ea5e9", - avatar: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E", + avatar: "/showcase-images/avatar-engineering.svg", }, { value: "growth", @@ -889,8 +887,7 @@ function advancedSelectConfiguration(variation) { description: "Marketing and customer growth", icon: "icon-[lucide--trending-up]", color: "#10b981", - avatar: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E", + avatar: "/showcase-images/avatar-growth.svg", }, { value: "support", label: "Support", description: "Customer operations", disabled: false }, ]; @@ -985,8 +982,7 @@ function comboboxConfiguration(variation) { description: "Product and visual design", icon: "icon-[lucide--palette]", color: "#8b5cf6", - avatar: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%238b5cf6'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3ED%3C/text%3E%3C/svg%3E", + avatar: "/showcase-images/avatar-design.svg", }, { value: "engineering", @@ -994,8 +990,7 @@ function comboboxConfiguration(variation) { description: "Platform and application engineering", icon: "icon-[lucide--code-2]", color: "#0ea5e9", - avatar: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%230ea5e9'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EE%3C/text%3E%3C/svg%3E", + avatar: "/showcase-images/avatar-engineering.svg", }, { value: "growth", @@ -1003,8 +998,7 @@ function comboboxConfiguration(variation) { description: "Marketing and customer growth", icon: "icon-[lucide--trending-up]", color: "#10b981", - avatar: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='32' fill='%2310b981'/%3E%3Ctext x='32' y='40' text-anchor='middle' font-family='Arial' font-size='24' fill='white'%3EG%3C/text%3E%3C/svg%3E", + avatar: "/showcase-images/avatar-growth.svg", }, { value: "support", diff --git a/examples/component-showcase/scripts/showcase-profiles.mjs b/examples/component-showcase/scripts/showcase-profiles.mjs index ff9648f5..11a9cb19 100644 --- a/examples/component-showcase/scripts/showcase-profiles.mjs +++ b/examples/component-showcase/scripts/showcase-profiles.mjs @@ -830,8 +830,7 @@ export const componentProfiles = { { title: "Unified citizen portal", description: "Access trusted services from one responsive interface.", - image: - "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Crect width='800' height='450' fill='%231e1b4b'/%3E%3Ccircle cx='610' cy='130' r='120' fill='%237c3aed' opacity='.55'/%3E%3Crect x='90' y='90' width='430' height='270' rx='28' fill='%23ffffff' opacity='.12'/%3E%3C/svg%3E", + image: "/showcase-images/dashboard-preview.svg", imageAlt: "Abstract dashboard preview", imagePosition: "top", imageAspect: "wide", @@ -1813,7 +1812,7 @@ export const componentProfiles = { "Responsive content image", "Render a responsive image with meaningful alternative text.", { - src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 450'%3E%3Crect width='800' height='450' fill='%231e1b4b'/%3E%3Ccircle cx='400' cy='225' r='120' fill='%237c3aed'/%3E%3C/svg%3E", + src: "/showcase-images/violet-circle.svg", alt: "Abstract violet circle on a dark surface", width: "800", height: "450", @@ -1823,7 +1822,7 @@ export const componentProfiles = { "Rounded thumbnail", "Use a rounded image in cards, directories, and compact summaries.", { - src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 400 400'%3E%3Crect width='400' height='400' fill='%230284c7'/%3E%3Cpath d='M80 290 190 120l130 170z' fill='white' opacity='.7'/%3E%3C/svg%3E", + src: "/showcase-images/blue-mountain.svg", alt: "Abstract blue thumbnail", width: "400", height: "400", @@ -1835,7 +1834,7 @@ export const componentProfiles = { "Eager hero image", "Use explicit dimensions and eager loading for an above-the-fold visual.", { - src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1200 600'%3E%3Crect width='1200' height='600' fill='%23059669'/%3E%3Crect x='150' y='120' width='900' height='360' rx='36' fill='white' opacity='.18'/%3E%3C/svg%3E", + src: "/showcase-images/green-hero.svg", alt: "Abstract green hero visual", width: "1200", height: "600", diff --git a/examples/component-showcase/showcase-manifest.json b/examples/component-showcase/showcase-manifest.json index 6177d9b9..b817a9ef 100644 --- a/examples/component-showcase/showcase-manifest.json +++ b/examples/component-showcase/showcase-manifest.json @@ -196,6 +196,18 @@ "events": ["click", "select", "change"], "profiled": false }, + { + "name": "CTASection", + "mount": "CTASection", + "slug": "ctasection", + "category": "marketing", + "purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.", + "demoCount": 3, + "propCount": 25, + "slots": ["default", "actions", "visual", "footer"], + "events": [], + "profiled": true + }, { "name": "Card", "mount": "Card", @@ -375,18 +387,6 @@ "events": ["copy", "success", "error"], "profiled": false }, - { - "name": "CTASection", - "mount": "CTASection", - "slug": "ctasection", - "category": "marketing", - "purpose": "Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.", - "demoCount": 3, - "propCount": 25, - "slots": ["default", "actions", "visual", "footer"], - "events": [], - "profiled": true - }, { "name": "CustomScrollbar", "mount": "CustomScrollbar", @@ -1167,18 +1167,6 @@ "events": [], "profiled": true }, - { - "name": "Textarea", - "mount": "Textarea", - "slug": "textarea", - "category": "forms", - "purpose": "Theme-aware, responsive textarea component.", - "demoCount": 3, - "propCount": 23, - "slots": [], - "events": ["input", "change", "focus", "blur", "invalid"], - "profiled": false - }, { "name": "TextLink", "mount": "TextLink", @@ -1192,16 +1180,16 @@ "profiled": true }, { - "name": "Timeline", - "mount": "Timeline", - "slug": "timeline", - "category": "base", - "purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata.", + "name": "Textarea", + "mount": "Textarea", + "slug": "textarea", + "category": "forms", + "purpose": "Theme-aware, responsive textarea component.", "demoCount": 3, - "propCount": 7, - "slots": ["default"], - "events": [], - "profiled": true + "propCount": 23, + "slots": [], + "events": ["input", "change", "focus", "blur", "invalid"], + "profiled": false }, { "name": "TimePicker", @@ -1215,6 +1203,18 @@ "events": ["input", "change", "focus", "blur", "open", "close", "invalid"], "profiled": false }, + { + "name": "Timeline", + "mount": "Timeline", + "slug": "timeline", + "category": "base", + "purpose": "Present responsive chronological activity, milestones, or workflow status with rich item metadata.", + "demoCount": 3, + "propCount": 7, + "slots": ["default"], + "events": [], + "profiled": true + }, { "name": "Toast", "mount": "Toast", diff --git a/package.json b/package.json index d07e41fc..8bff0eb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wrnexus", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "description": "An SSR-first full-stack web framework with server-rendered reactive components. Bun-first, Node-friendly.", @@ -32,7 +32,16 @@ "auth:dev": "bun run --cwd examples/auth-showcase dev", "auth:check": "bun run --cwd packages/auth check", "validate:auth": "node scripts/validate-auth.mjs", - "validate:0.6": "node scripts/validate-0.6.mjs" + "validate:0.6": "node scripts/validate-0.6.mjs", + "validate:0.7": "node scripts/validate-0.7.mjs", + "security:framework": "node scripts/security-performance-audit.mjs", + "sbom": "node scripts/generate-sbom.mjs", + "benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs", + "check:production": "bun run check:workspace && bun run validate:0.7 && bun run security:framework && bun run check", + "validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs", + "repair:workspace": "node scripts/repair-workspace.mjs", + "test:workspace-repair": "node scripts/test-workspace-repair.mjs", + "check:workspace": "node scripts/repair-workspace.mjs --check" }, "devDependencies": { "@eslint/js": "latest", diff --git a/packages/ai/package.json b/packages/ai/package.json index c6b535bb..1f5113fb 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/auth/package.json b/packages/auth/package.json index 118a2c37..64b35042 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/auth", - "version": "0.6.0", + "version": "0.7.0", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.", "type": "module", "sideEffects": false, diff --git a/packages/authz/package.json b/packages/authz/package.json index 8371988b..6fe387f9 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/benchmark/README.md b/packages/benchmark/README.md new file mode 100644 index 00000000..f312ae5e --- /dev/null +++ b/packages/benchmark/README.md @@ -0,0 +1,9 @@ +# @wrnexus/benchmark + +Deterministic benchmark execution, percentiles, baseline comparisons, and regression budgets for builds, SSR, hydration, stores, and application hot paths. + +```ts +import { runBenchmark, assertBenchmarkBudget } from "@wrnexus/benchmark"; +const result = await runBenchmark("render", render, { iterations: 100 }); +assertBenchmarkBudget(result, baseline, { p95Percent: 5 }); +``` diff --git a/packages/benchmark/package.json b/packages/benchmark/package.json new file mode 100644 index 00000000..d0d761e2 --- /dev/null +++ b/packages/benchmark/package.json @@ -0,0 +1,10 @@ +{ + "name": "@wrnexus/benchmark", + "version": "0.7.0", + "type": "module", + "description": "Deterministic benchmark runner and performance regression budgets for WRNexusJS.", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + } +} diff --git a/packages/benchmark/src/index.ts b/packages/benchmark/src/index.ts new file mode 100644 index 00000000..e2a498f5 --- /dev/null +++ b/packages/benchmark/src/index.ts @@ -0,0 +1,145 @@ +export interface BenchmarkOptions { + iterations?: number; + warmup?: number; + clock?: () => number; + setup?: () => void | Promise; + teardown?: () => void | Promise; +} + +export interface BenchmarkResult { + name: string; + iterations: number; + totalMs: number; + meanMs: number; + minMs: number; + maxMs: number; + p50Ms: number; + p95Ms: number; + p99Ms: number; + operationsPerSecond: number; + samples: number[]; +} + +export interface RegressionBudget { + meanPercent?: number; + p95Percent?: number; + maxAbsoluteMs?: number; + minOperationsPerSecond?: number; +} + +export interface RegressionViolation { + metric: "meanMs" | "p95Ms" | "maxMs" | "operationsPerSecond"; + baseline?: number; + current: number; + limit: number; + message: string; +} + +export function percentile(values: readonly number[], quantile: number): number { + if (!values.length) return 0; + const sorted = [...values].sort((a, b) => a - b); + const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(quantile * sorted.length) - 1)); + return sorted[index]!; +} + +export async function runBenchmark( + name: string, + operation: () => void | Promise, + options: BenchmarkOptions = {}, +): Promise { + const iterations = Math.max(1, Math.floor(options.iterations ?? 100)); + const warmup = Math.max(0, Math.floor(options.warmup ?? Math.min(10, iterations))); + const clock = options.clock ?? (() => performance.now()); + await options.setup?.(); + try { + for (let index = 0; index < warmup; index++) await operation(); + const samples: number[] = []; + for (let index = 0; index < iterations; index++) { + const start = clock(); + await operation(); + samples.push(Math.max(0, clock() - start)); + } + const totalMs = samples.reduce((sum, sample) => sum + sample, 0); + const meanMs = totalMs / samples.length; + return { + name, + iterations, + totalMs, + meanMs, + minMs: Math.min(...samples), + maxMs: Math.max(...samples), + p50Ms: percentile(samples, 0.5), + p95Ms: percentile(samples, 0.95), + p99Ms: percentile(samples, 0.99), + operationsPerSecond: meanMs === 0 ? Number.POSITIVE_INFINITY : 1000 / meanMs, + samples, + }; + } finally { + await options.teardown?.(); + } +} + +export function compareBenchmark( + current: BenchmarkResult, + baseline: BenchmarkResult | undefined, + budget: RegressionBudget = {}, +): RegressionViolation[] { + const violations: RegressionViolation[] = []; + if (baseline && budget.meanPercent !== undefined) { + const limit = baseline.meanMs * (1 + budget.meanPercent / 100); + if (current.meanMs > limit) { + violations.push({ + metric: "meanMs", + baseline: baseline.meanMs, + current: current.meanMs, + limit, + message: `Mean latency regressed by more than ${budget.meanPercent}%.`, + }); + } + } + if (baseline && budget.p95Percent !== undefined) { + const limit = baseline.p95Ms * (1 + budget.p95Percent / 100); + if (current.p95Ms > limit) { + violations.push({ + metric: "p95Ms", + baseline: baseline.p95Ms, + current: current.p95Ms, + limit, + message: `P95 latency regressed by more than ${budget.p95Percent}%.`, + }); + } + } + if (budget.maxAbsoluteMs !== undefined && current.maxMs > budget.maxAbsoluteMs) { + violations.push({ + metric: "maxMs", + current: current.maxMs, + limit: budget.maxAbsoluteMs, + message: `Maximum latency exceeds ${budget.maxAbsoluteMs} ms.`, + }); + } + if ( + budget.minOperationsPerSecond !== undefined && + current.operationsPerSecond < budget.minOperationsPerSecond + ) { + violations.push({ + metric: "operationsPerSecond", + current: current.operationsPerSecond, + limit: budget.minOperationsPerSecond, + message: `Throughput is below ${budget.minOperationsPerSecond} operations/second.`, + }); + } + return violations; +} + +export function assertBenchmarkBudget( + current: BenchmarkResult, + baseline: BenchmarkResult | undefined, + budget: RegressionBudget, +): void { + const violations = compareBenchmark(current, baseline, budget); + if (violations.length) { + throw new Error( + `Benchmark '${current.name}' failed:\n${violations.map((item) => `- ${item.message}`).join("\n")}`, + ); + } +} diff --git a/packages/benchmark/test/benchmark.test.ts b/packages/benchmark/test/benchmark.test.ts new file mode 100644 index 00000000..f849629e --- /dev/null +++ b/packages/benchmark/test/benchmark.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from "bun:test"; +import { compareBenchmark, percentile, runBenchmark } from "../src/index.ts"; + +describe("@wrnexus/benchmark", () => { + test("calculates percentiles and deterministic samples", async () => { + let now = 0; + const result = await runBenchmark( + "clock", + () => { + now += 2; + }, + { + iterations: 3, + warmup: 0, + clock: () => now, + }, + ); + expect(result.samples).toEqual([2, 2, 2]); + expect(result.p95Ms).toBe(2); + expect(percentile([1, 2, 3, 4], 0.5)).toBe(2); + }); + + test("reports regression budget violations", () => { + const baseline = { meanMs: 10, p95Ms: 20 } as any; + const current = { meanMs: 12, p95Ms: 30, maxMs: 40, operationsPerSecond: 50 } as any; + expect(compareBenchmark(current, baseline, { meanPercent: 10, p95Percent: 20 })).toHaveLength( + 2, + ); + }); +}); diff --git a/packages/cache/README.md b/packages/cache/README.md new file mode 100644 index 00000000..c33f6556 --- /dev/null +++ b/packages/cache/README.md @@ -0,0 +1,9 @@ +# @wrnexus/cache + +Bounded in-memory/tag caching and HTTP response caching for WRNexusJS. Supports request deduplication, tag invalidation, ETags, fresh/stale states, and optional detached stale revalidation. + +```ts +import { TagCache, responseCache } from "@wrnexus/cache"; +const cache = new TagCache({ ttlMs: 60_000, staleWhileRevalidateMs: 300_000 }); +export default responseCache({ cache, tags: ["products"] }); +``` diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 00000000..bd88d8e4 --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,13 @@ +{ + "name": "@wrnexus/cache", + "version": "0.7.0", + "type": "module", + "description": "Tag-aware memory and response caching with stale-while-revalidate for WRNexusJS.", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@wrnexus/core": "workspace:*" + } +} diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts new file mode 100644 index 00000000..886ae92a --- /dev/null +++ b/packages/cache/src/index.ts @@ -0,0 +1,4 @@ +export { TagCache } from "./memory.ts"; +export type { CacheEntry, CacheLookup, CacheSetOptions, TagCacheOptions } from "./memory.ts"; +export { responseCache } from "./response.ts"; +export type { CachedResponse, ResponseCacheOptions } from "./response.ts"; diff --git a/packages/cache/src/memory.ts b/packages/cache/src/memory.ts new file mode 100644 index 00000000..0e5ff813 --- /dev/null +++ b/packages/cache/src/memory.ts @@ -0,0 +1,151 @@ +export interface CacheEntry { + value: V; + createdAt: number; + expiresAt: number; + staleUntil: number; + tags: string[]; +} + +export type CacheLookup = { state: "miss" } | { state: "fresh" | "stale"; entry: CacheEntry }; + +export interface CacheSetOptions { + ttlMs?: number; + staleWhileRevalidateMs?: number; + tags?: string[]; +} + +export interface TagCacheOptions { + ttlMs?: number; + staleWhileRevalidateMs?: number; + maxEntries?: number; + clock?: () => number; +} + +export class TagCache { + private entries = new Map>(); + private tagIndex = new Map>(); + private pending = new Map>(); + private readonly ttlMs: number; + private readonly staleMs: number; + private readonly maxEntries: number; + private readonly clock: () => number; + + constructor(options: TagCacheOptions = {}) { + this.ttlMs = options.ttlMs ?? 60_000; + this.staleMs = options.staleWhileRevalidateMs ?? 0; + this.maxEntries = Math.max(1, options.maxEntries ?? 10_000); + this.clock = options.clock ?? Date.now; + } + + lookup(key: string): CacheLookup { + const entry = this.entries.get(key); + if (!entry) return { state: "miss" }; + const now = this.clock(); + if (entry.staleUntil <= now) { + this.delete(key); + return { state: "miss" }; + } + this.entries.delete(key); + this.entries.set(key, entry); + return { state: entry.expiresAt > now ? "fresh" : "stale", entry }; + } + + get(key: string): V | undefined { + const hit = this.lookup(key); + return hit.state === "miss" ? undefined : hit.entry.value; + } + + set(key: string, value: V, options: CacheSetOptions = {}): void { + this.delete(key); + const now = this.clock(); + const ttlMs = Math.max(0, options.ttlMs ?? this.ttlMs); + const staleMs = Math.max(0, options.staleWhileRevalidateMs ?? this.staleMs); + const tags = [...new Set(options.tags ?? [])]; + const entry: CacheEntry = { + value, + createdAt: now, + expiresAt: now + ttlMs, + staleUntil: now + ttlMs + staleMs, + tags, + }; + this.entries.set(key, entry); + for (const tag of tags) { + const keys = this.tagIndex.get(tag) ?? new Set(); + keys.add(key); + this.tagIndex.set(tag, keys); + } + while (this.entries.size > this.maxEntries) { + const oldest = this.entries.keys().next().value as string | undefined; + if (oldest === undefined) break; + this.delete(oldest); + } + } + + async getOrLoad( + key: string, + loader: () => V | Promise, + options: CacheSetOptions = {}, + ): Promise { + const hit = this.lookup(key); + if (hit.state === "fresh") return hit.entry.value; + if (hit.state === "stale") { + if (!this.pending.has(key)) { + const refresh = Promise.resolve() + .then(loader) + .then((value) => { + this.set(key, value, options); + return value; + }) + .finally(() => this.pending.delete(key)); + this.pending.set(key, refresh); + } + return hit.entry.value; + } + const existing = this.pending.get(key); + if (existing) return existing; + const pending = Promise.resolve() + .then(loader) + .then((value) => { + this.set(key, value, options); + return value; + }) + .finally(() => this.pending.delete(key)); + this.pending.set(key, pending); + return pending; + } + + delete(key: string): boolean { + const entry = this.entries.get(key); + if (!entry) return false; + this.entries.delete(key); + for (const tag of entry.tags) { + const keys = this.tagIndex.get(tag); + keys?.delete(key); + if (keys?.size === 0) this.tagIndex.delete(tag); + } + return true; + } + + invalidateTag(tag: string): number { + const keys = [...(this.tagIndex.get(tag) ?? [])]; + for (const key of keys) this.delete(key); + return keys.length; + } + + invalidateTags(tags: Iterable): number { + const keys = new Set(); + for (const tag of tags) for (const key of this.tagIndex.get(tag) ?? []) keys.add(key); + for (const key of keys) this.delete(key); + return keys.size; + } + + clear(): void { + this.entries.clear(); + this.tagIndex.clear(); + this.pending.clear(); + } + + get size(): number { + return this.entries.size; + } +} diff --git a/packages/cache/src/response.ts b/packages/cache/src/response.ts new file mode 100644 index 00000000..0dc921ed --- /dev/null +++ b/packages/cache/src/response.ts @@ -0,0 +1,109 @@ +import type { Context, Middleware } from "@wrnexus/core"; +import { etag, notModified } from "@wrnexus/core"; +import { TagCache, type CacheSetOptions } from "./memory.ts"; + +export interface CachedResponse { + status: number; + statusText: string; + headers: [string, string][]; + body: Uint8Array; + etag: string; +} + +export interface ResponseCacheOptions extends CacheSetOptions { + cache?: TagCache; + key?: (ctx: Context) => string; + vary?: string[]; + shouldCache?: (ctx: Context, response: Response) => boolean; + /** + * Optional detached revalidator used for stale-while-revalidate. Middleware + * `next()` is deliberately never called after a response has been returned, + * because many middleware pipelines are single-use. + */ + revalidate?: (ctx: Context) => Promise; + onRevalidateError?: (error: unknown, ctx: Context) => void; +} + +function defaultKey(ctx: Context, vary: string[]): string { + const values = vary.map((name) => `${name.toLowerCase()}=${ctx.req.headers.get(name) ?? ""}`); + return `${ctx.req.method}:${ctx.url.origin}${ctx.url.pathname}${ctx.url.search}|${values.join("|")}`; +} + +function cacheable(ctx: Context, response: Response): boolean { + if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") return false; + if (response.status < 200 || response.status >= 400) return false; + if (response.headers.has("set-cookie")) return false; + const control = response.headers.get("cache-control") ?? ""; + return !/(?:^|,)\s*(?:no-store|private)(?:\s|,|$)/i.test(control); +} + +function fromCached(ctx: Context, cached: CachedResponse, state: "fresh" | "stale"): Response { + const headers = new Headers(cached.headers); + headers.set("x-wrnexus-cache", state === "fresh" ? "HIT" : "STALE"); + headers.set("age", "0"); + if (notModified(ctx.req, cached.etag)) { + return new Response(null, { status: 304, headers }); + } + return new Response(ctx.req.method === "HEAD" ? null : cached.body.slice(), { + status: cached.status, + statusText: cached.statusText, + headers, + }); +} + +async function capture(response: Response): Promise { + const body = new Uint8Array(await response.clone().arrayBuffer()); + const tag = response.headers.get("etag") ?? etag(body); + const headers = new Headers(response.headers); + headers.set("etag", tag); + return { + status: response.status, + statusText: response.statusText, + headers: [...headers.entries()], + body, + etag: tag, + }; +} + +export function responseCache(options: ResponseCacheOptions = {}): Middleware { + const cache = options.cache ?? new TagCache(); + const vary = options.vary ?? ["accept-encoding", "accept-language"]; + return async (ctx, next) => { + if (ctx.req.method !== "GET" && ctx.req.method !== "HEAD") return next(); + const key = options.key?.(ctx) ?? defaultKey(ctx, vary); + const hit = cache.lookup(key); + if (hit.state === "fresh") return fromCached(ctx, hit.entry.value, "fresh"); + if (hit.state === "stale") { + if (options.revalidate) { + void options + .revalidate(ctx) + .then(async (response) => { + if ((options.shouldCache ?? cacheable)(ctx, response)) { + cache.set(key, await capture(response), options); + } + }) + .catch((error: unknown) => options.onRevalidateError?.(error, ctx)); + } + return fromCached(ctx, hit.entry.value, "stale"); + } + + const response = await next(); + if (!(options.shouldCache ?? cacheable)(ctx, response)) { + try { + response.headers.set("x-wrnexus-cache", "BYPASS"); + } catch { + // Immutable response. + } + return response; + } + const stored = await capture(response); + cache.set(key, stored, options); + const headers = new Headers(stored.headers); + headers.set("x-wrnexus-cache", "MISS"); + return new Response(ctx.req.method === "HEAD" ? null : stored.body.slice(), { + status: stored.status, + statusText: stored.statusText, + headers, + }); + }; +} diff --git a/packages/cache/test/cache.test.ts b/packages/cache/test/cache.test.ts new file mode 100644 index 00000000..1fa6588e --- /dev/null +++ b/packages/cache/test/cache.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, test } from "bun:test"; +import { TagCache, responseCache } from "../src/index.ts"; + +describe("@wrnexus/cache", () => { + test("supports fresh, stale, and tag invalidation", async () => { + let now = 0; + const cache = new TagCache({ ttlMs: 10, staleWhileRevalidateMs: 10, clock: () => now }); + cache.set("a", 1, { tags: ["users"] }); + expect(cache.lookup("a").state).toBe("fresh"); + now = 11; + expect(cache.lookup("a").state).toBe("stale"); + expect(cache.invalidateTag("users")).toBe(1); + expect(cache.lookup("a").state).toBe("miss"); + }); + + test("deduplicates concurrent loaders", async () => { + const cache = new TagCache(); + let calls = 0; + const loader = async () => ++calls; + const [a, b] = await Promise.all([cache.getOrLoad("x", loader), cache.getOrLoad("x", loader)]); + expect(a).toBe(1); + expect(b).toBe(1); + expect(calls).toBe(1); + }); + + test("response middleware does not call next twice for stale entries", async () => { + let now = 0; + const cache = new TagCache({ ttlMs: 1, staleWhileRevalidateMs: 100, clock: () => now }); + const middleware = responseCache({ cache, ttlMs: 1, staleWhileRevalidateMs: 100 }); + let calls = 0; + const ctx = { + req: new Request("https://example.com/a"), + url: new URL("https://example.com/a"), + } as any; + await middleware(ctx, async () => { + calls++; + return new Response("one"); + }); + now = 2; + const stale = await middleware(ctx, async () => { + calls++; + return new Response("two"); + }); + expect(await stale.text()).toBe("one"); + expect(calls).toBe(1); + }); +}); diff --git a/packages/captcha/package.json b/packages/captcha/package.json index 34101664..d25c5d6f 100644 --- a/packages/captcha/package.json +++ b/packages/captcha/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/captcha", - "version": "0.6.0", + "version": "0.7.0", "description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.", "type": "module", "sideEffects": false, diff --git a/packages/cli/package.json b/packages/cli/package.json index 104585ee..e579860e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/build.ts b/packages/cli/src/build.ts index 65e8c609..948dd584 100644 --- a/packages/cli/src/build.ts +++ b/packages/cli/src/build.ts @@ -24,7 +24,13 @@ import { import { basename, extname, join, resolve } from "node:path"; import { buildRouter, type Route } from "@wrnexus/router"; import { getReactiveRuntime } from "@wrnexus/csr"; -import { assertValidAst, generate, parse } from "@wrnexus/compiler"; +import { + analyzeRuntimeRequirements, + assertValidAst, + generate, + parse, + type RuntimeRequirements, +} from "@wrnexus/compiler"; import { loadAppConfig, headToString, @@ -102,12 +108,14 @@ export async function runBuild(appRoot: string): Promise { // keep the exact compiler path and output contract by default. let compiledCount = 0; const compiledFiles = new Map(); + const runtimeAnalysis = new Map(); const compileWrn = async (file: string): Promise => { if (!file.endsWith(".wrn") || compiledFiles.has(file)) return; const source = readFileSync(file, "utf8"); let ast = parse(source); assertValidAst(ast, { file, accessibility: true }); ast = await pluginRunner.transformAst(ast, file); + runtimeAnalysis.set(file, analyzeRuntimeRequirements(ast)); const pluginDiagnostics = await pluginRunner.diagnostics(ast, file); const errors = pluginDiagnostics.filter((diagnostic) => diagnostic.severity === "error"); for (const diagnostic of pluginDiagnostics.filter((item) => item.severity !== "error")) { @@ -453,6 +461,7 @@ await createProductionServer( ], runtimeFile: reactivePath, cssFile: hasStyles ? join(distDir, "styles.css") : join(distDir, "framework.css"), + runtimeAnalysis, }); report.pluginAssets = emittedPluginAssets.assets.map((asset) => ({ id: asset.id, @@ -636,6 +645,11 @@ interface BuildReport { source: string; sourceBytes: number; dynamicParams: string[]; + execution: RuntimeRequirements["kind"]; + canPrerender: boolean; + needsClientRuntime: boolean; + needsServerRuntime: boolean; + hydrationStrategy: string | null; }>; assets: Array<{ file: string; bytes: number }>; measurements: { routeJsBytes: number; routeCssBytes: number; imageBytes: number }; @@ -666,6 +680,7 @@ function createBuildReport(input: { routes: Array<{ kind: "page" | "api" | "realtime"; route: Route }>; runtimeFile: string; cssFile: string; + runtimeAnalysis: ReadonlyMap; }): BuildReport { const assets = walkFiles(input.distDir) .filter((file) => !file.endsWith("build-report.json") && !fwd(file).includes("/compiled/")) @@ -689,6 +704,11 @@ function createBuildReport(input: { source: fwd(route.file.replace(input.root, "").replace(/^\//, "")), sourceBytes: fileBytes(route.file), dynamicParams: route.paramNames, + execution: input.runtimeAnalysis.get(route.file)?.kind ?? "dynamic", + canPrerender: input.runtimeAnalysis.get(route.file)?.canPrerender ?? false, + needsClientRuntime: input.runtimeAnalysis.get(route.file)?.needsClientRuntime ?? true, + needsServerRuntime: input.runtimeAnalysis.get(route.file)?.needsServerRuntime ?? true, + hydrationStrategy: input.runtimeAnalysis.get(route.file)?.hydrationStrategy ?? null, })), assets, measurements: { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 0ce1b496..80cff04a 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -563,6 +563,71 @@ function writeMigrationReport(ctx: MigrationCtx): void { * Versioned, idempotent upgrade steps. Each MUST be safe to re-run. Source * migrations must preserve semantics and are protected by update backups. */ + +function updateV070Config(ctx: MigrationCtx): void { + const candidates = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]; + const file = candidates.map((name) => join(ctx.appRoot, name)).find(existsSync); + if (!file) return; + const current = readFileSync(file, "utf8"); + const additions: string[] = []; + if (!/\bnavigation\s*:/.test(current)) { + additions.push(` navigation: { + mode: "auto", + }`); + } + if (!/\bsecurity\s*:/.test(current)) { + additions.push(` security: { + headers: true, + requestLimits: { + maxUrlLength: 8192, + maxHeaderCount: 100, + maxHeaderBytes: 32768, + maxQueryParameters: 100, + maxBodyBytes: 10485760, + timeoutMs: 30000, + maxConcurrent: 1000, + fetchMetadata: true, + }, + contentSecurityPolicy: { enabled: true, useDefaults: true }, + trustedTypes: { enabled: true, requireForScript: true }, + hsts: { enabled: true, maxAge: 63072000, includeSubDomains: true, preload: true }, + }`); + } + if (!/\bperformance\s*:/.test(current)) { + additions.push(` performance: { + enforcement: "warn", + analyze: true, + budgets: { + routeJsBytes: 51200, + routeCssBytes: 25600, + htmlBytes: 204800, + hydrationMs: 200, + serverRenderMs: 500, + lcpMs: 2500, + inpMs: 200, + cls: 0.1, + ttfbMs: 800, + }, + }`); + } + if (!/\bobservability\s*:/.test(current)) { + additions.push(` observability: { + enabled: true, + serverTiming: true, + sampleRate: 0.1, + exporter: "none", + webVitals: true, + }`); + } + if (!additions.length) return; + const index = current.lastIndexOf("}"); + if (index < 0) return; + const before = current.slice(0, index).replace(/,?\s*$/, ""); + const next = `${before},\n${additions.join(",\n")}\n${current.slice(index)}`; + ctx.log(`~ ${file.slice(ctx.appRoot.length + 1)}: security/performance production defaults`); + if (!ctx.dryRun) writeFileSync(file, next, "utf8"); +} + const MIGRATIONS: Migration[] = [ { version: "0.2.8", @@ -1737,6 +1802,61 @@ const MIGRATIONS: Migration[] = [ writeMigrationReport(ctx); }, }, + { + version: "0.7.0", + id: "0.7.0-01-security-performance-foundation", + description: + "Adds secure-by-default request limits, caching, image optimization, observability, benchmarks, and production hardening packages.", + apply(ctx) { + const file = join(ctx.appRoot, "package.json"); + if (existsSync(file)) { + const pkg = JSON.parse(readFileSync(file, "utf8")) as Record; + const dependencies = (pkg.dependencies ??= {}); + const devDependencies = (pkg.devDependencies ??= {}); + const added: string[] = []; + for (const name of [ + "@wrnexus/security", + "@wrnexus/cache", + "@wrnexus/image", + "@wrnexus/observability", + ]) { + if (dependencies[name] === `^${ctx.to}`) continue; + dependencies[name] = `^${ctx.to}`; + added.push(name); + } + if (devDependencies["@wrnexus/benchmark"] !== `^${ctx.to}`) { + devDependencies["@wrnexus/benchmark"] = `^${ctx.to}`; + added.push("@wrnexus/benchmark (dev)"); + } + if (added.length) { + ctx.log(`+ security/performance dependencies: ${added.join(", ")}`); + if (!ctx.dryRun) writeFileSync(file, JSON.stringify(pkg, null, 2) + "\n", "utf8"); + } + } + updateV070Config(ctx); + const gitignore = join(ctx.appRoot, ".gitignore"); + const currentIgnore = existsSync(gitignore) ? readFileSync(gitignore, "utf8") : ""; + const ignoreEntries = [".env", ".env.*", "!.env.example", "!.env.*.example"]; + const knownIgnore = new Set(currentIgnore.split(/\r?\n/).map((line) => line.trim())); + const missingIgnore = ignoreEntries.filter((entry) => !knownIgnore.has(entry)); + if (missingIgnore.length) { + ctx.log(`+ .gitignore secure environment templates: ${missingIgnore.join(", ")}`); + if (!ctx.dryRun) { + writeFileSync( + gitignore, + `${currentIgnore.replace(/\s*$/, "")}\n${missingIgnore.join("\n")}\n`.replace( + /^\n+/, + "", + ), + "utf8", + ); + } + } + const review = + "Review WRNexusJS 0.7 security policy, CSP allowlists, tenant authorization, upload scanners, cache adapters, and production performance budgets."; + if (!ctx.report.needsReview.includes(review)) ctx.report.needsReview.push(review); + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 631b17ff..833df827 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/compiler/src/analysis.ts b/packages/compiler/src/analysis.ts new file mode 100644 index 00000000..aa9b7322 --- /dev/null +++ b/packages/compiler/src/analysis.ts @@ -0,0 +1,78 @@ +import type { PageAst, ViewNode } from "@wrnexus/syntax"; + +export type RouteExecutionKind = + | "static" + | "static-interactive" + | "request-ssr" + | "authenticated-ssr" + | "streaming-ssr" + | "dynamic"; + +export interface RuntimeRequirements { + kind: RouteExecutionKind; + canPrerender: boolean; + needsClientRuntime: boolean; + needsServerRuntime: boolean; + hydrationStrategy: string | null; + reasons: string[]; +} + +function hasEvent(nodes: ViewNode[]): boolean { + for (const node of nodes) { + if (node.type === "element") { + if (node.attrs.some((attribute) => attribute.event)) return true; + if (hasEvent(node.children)) return true; + } else if (node.type === "each") { + if (hasEvent(node.body) || hasEvent(node.empty)) return true; + } else if (node.type === "if") { + if (node.branches.some((branch) => hasEvent(branch.body))) return true; + } + } + return false; +} + +export function analyzeRuntimeRequirements(ast: PageAst): RuntimeRequirements { + const reasons: string[] = []; + const clientFunctions = ast.runtimeFunctions.some((fn) => fn.runtime !== "server"); + const clientState = ast.states.some((state) => state.runtime !== "server"); + const interactive = + clientFunctions || + clientState || + ast.effects.length > 0 || + ast.watches.length > 0 || + hasEvent(ast.view); + if (interactive) reasons.push("client interactivity"); + + const requestData = + ast.loads.length > 0 || + ast.actions.length > 0 || + ast.dataApis.length > 0 || + ast.apis.length > 0 || + ast.realtimes.length > 0 || + ast.runtimeFunctions.some((fn) => fn.runtime === "server") || + ast.states.some((state) => state.runtime === "server"); + if (requestData) reasons.push("server/request data"); + + const authenticated = /^(?:required|true)$/i.test(ast.security.auth ?? ""); + if (authenticated) reasons.push("authentication required"); + + const streaming = /^(?:true|required)$/i.test(ast.security.streaming ?? ""); + if (streaming) reasons.push("streaming enabled"); + + let kind: RouteExecutionKind; + if (streaming) kind = "streaming-ssr"; + else if (authenticated) kind = "authenticated-ssr"; + else if (requestData && interactive) kind = "dynamic"; + else if (requestData) kind = "request-ssr"; + else if (interactive) kind = "static-interactive"; + else kind = "static"; + + return { + kind, + canPrerender: kind === "static" || kind === "static-interactive", + needsClientRuntime: interactive && ast.hydrate !== "none" && ast.runtime !== "server", + needsServerRuntime: requestData || authenticated || streaming || ast.runtime === "server", + hydrationStrategy: interactive ? (ast.hydrate ?? "load") : null, + reasons, + }; +} diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index 72461ab5..663d84b5 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -77,6 +77,38 @@ function isHtmlBooleanAttribute(name: string): boolean { return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase()); } +const URL_ATTRIBUTES = new Set([ + "href", + "src", + "action", + "formaction", + "poster", + "cite", + "background", + "xlink:href", +]); + +function stripAsciiControlAndSpace(value: string): string { + let result = ""; + for (const character of value) { + if (character.charCodeAt(0) > 0x20) result += character; + } + return result; +} + +function sanitizeUrlAttribute(value: string): string { + const compact = stripAsciiControlAndSpace(value.trim()); + const lower = compact.toLowerCase(); + if (/^(?:javascript|vbscript|file):/.test(lower)) return "about:blank"; + if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(lower)) return "about:blank"; + return value; +} + +function safeAttributeValue(name: string, value: string): string { + if (!URL_ATTRIBUTES.has(name.toLowerCase()) || value.includes("{")) return value; + return sanitizeUrlAttribute(value); +} + /** Escape a value placed inside a double-quoted HTML attribute. */ function attrEscape(value: string): string { return value @@ -110,7 +142,9 @@ function renderAttr(attr: Attr): string { case "csrText": return ""; default: - return attr.boolean ? ` ${attr.name}` : ` ${attr.name}="${attrEscape(attr.value)}"`; + return attr.boolean + ? ` ${attr.name}` + : ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`; } } @@ -176,7 +210,7 @@ function renderAttrs( const initial = reactiveAttrValue(attr.value, reactive); if (initial === null) return base; const marker = JSON.stringify([attr.name, attr.value]); - return ` ${attr.name}="${attrEscape(initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`; + return ` ${attr.name}="${attrEscape(URL_ATTRIBUTES.has(attr.name.toLowerCase()) ? sanitizeUrlAttribute(initial) : initial)}" data-wrn-bind-${bindIndex++}="${attrEscape(marker)}"`; }) .join(""); return csrId ? `${rendered} data-wrnexus-csr="${attrEscape(csrId)}"` : rendered; @@ -2148,7 +2182,7 @@ function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): stri const expression = wholeAttributeExpression(attr.value); if (!expression) { - return ` ${attr.name}="${attrEscape(attr.value)}"`; + return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`; } dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts index 5b3bd823..c2791eb0 100644 --- a/packages/compiler/src/index.ts +++ b/packages/compiler/src/index.ts @@ -35,6 +35,8 @@ export { generateStoreBrowserModule, generateStoreModule } from "./store-codegen export { createComponentContract } from "./component-contract.ts"; export { resolveWrnImport, resolveWrnImports } from "./import-resolver.ts"; export { createWrnSourceMap } from "./source-map.ts"; +export { analyzeRuntimeRequirements } from "./analysis.ts"; +export type { RouteExecutionKind, RuntimeRequirements } from "./analysis.ts"; export { generateNative, NativeCompileError } from "./native-codegen.ts"; export { Lexer, LexError } from "@wrnexus/syntax"; export { eraseFunctionTypes, inferredRuntimeType, runtimeTypeOf } from "@wrnexus/syntax"; diff --git a/packages/compiler/src/store-codegen.ts b/packages/compiler/src/store-codegen.ts index dc0eebed..12b9e423 100644 --- a/packages/compiler/src/store-codegen.ts +++ b/packages/compiler/src/store-codegen.ts @@ -204,7 +204,7 @@ function __diagnostic(code, message, details) { } function __csrfToken() { if (typeof document === "undefined") return undefined; - const match = /(?:^|;\\s*)wrnexus_csrf=([^;]+)/.exec(document.cookie || ""); + const match = /(?:^|;\\s*)wire-csrf=([^;]+)/.exec(document.cookie || ""); return match ? decodeURIComponent(match[1]) : undefined; } async function __callServerFunction(storeName, functionName, args, options) { @@ -215,7 +215,7 @@ async function __callServerFunction(storeName, functionName, args, options) { method: "POST", credentials: "same-origin", signal: options.signal, - headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-wrnexus-csrf": csrf } : {}, options.headers || {}), + headers: Object.assign({ "content-type": "application/json", "x-request-id": traceId }, csrf ? { "x-csrf-token": csrf } : {}, options.headers || {}), body: JSON.stringify({ component: storeName, function: functionName, args: args }), }); const payload = await response.json().catch(function () { return null; }); diff --git a/packages/core/package.json b/packages/core/package.json index c954d0d0..b7122fd1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/src/cache.ts b/packages/core/src/cache.ts index 4d43fe99..3df3610e 100644 --- a/packages/core/src/cache.ts +++ b/packages/core/src/cache.ts @@ -89,6 +89,8 @@ export interface CacheControlOptions { noCache?: boolean; /** stale-while-revalidate window in seconds. */ staleWhileRevalidate?: number; + /** stale-if-error window in seconds. */ + staleIfError?: number; immutable?: boolean; } @@ -104,6 +106,9 @@ export function cacheControl(options: CacheControlOptions): string { if (options.staleWhileRevalidate !== undefined) { parts.push(`stale-while-revalidate=${Math.max(0, Math.floor(options.staleWhileRevalidate))}`); } + if (options.staleIfError !== undefined) { + parts.push(`stale-if-error=${Math.max(0, Math.floor(options.staleIfError))}`); + } if (options.immutable) parts.push("immutable"); return parts.join(", "); } diff --git a/packages/core/src/csrf.ts b/packages/core/src/csrf.ts index 60fca6da..4a267365 100644 --- a/packages/core/src/csrf.ts +++ b/packages/core/src/csrf.ts @@ -1,11 +1,6 @@ /** - * CSRF protection via the double-submit cookie pattern. - * - * The framework sets a readable `wire-csrf` cookie on page loads; the client - * echoes it in an `x-csrf-token` header on unsafe requests (the Wire UI form - * runtime does this automatically). The server checks header === cookie. A - * cross-site attacker can't read the cookie to forge the header, so the request - * is rejected — while same-origin requests pass. + * CSRF protection via the double-submit cookie pattern plus origin/fetch + * metadata validation for unsafe requests. */ import type { Context, Middleware } from "./context.ts"; @@ -15,12 +10,20 @@ export const CSRF_HEADER = "x-csrf-token"; const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]); +export interface CsrfProtectionOptions { + /** Validate Origin when present. Defaults to true. */ + verifyOrigin?: boolean; + /** Additional exact origins permitted for trusted cross-origin clients. */ + trustedOrigins?: string[]; + /** Reject Sec-Fetch-Site: cross-site on unsafe requests. Defaults to true. */ + verifyFetchMetadata?: boolean; +} + /** Ensure the CSRF cookie exists (readable by JS) and return its token. */ export function csrfToken(ctx: Context): string { let token = ctx.cookies.get(CSRF_COOKIE); if (!token) { token = crypto.randomUUID().replace(/-/g, ""); - // Readable by JS (double-submit needs it) but Secure on HTTPS. ctx.cookies.set(CSRF_COOKIE, token, { sameSite: "Lax", path: "/", @@ -30,33 +33,38 @@ export function csrfToken(ctx: Context): string { return token; } -/** - * Verify an unsafe request's CSRF token against the cookie. Safe methods - * (GET/HEAD/OPTIONS) always pass. The token may arrive in the `x-csrf-token` - * header or a `_csrf` field already parsed onto `ctx.locals`. - */ -export function verifyCsrf(ctx: Context): boolean { +/** Verify an unsafe request's token, origin, and browser fetch metadata. */ +export function verifyCsrf(ctx: Context, options: CsrfProtectionOptions = {}): boolean { if (SAFE_METHODS.has(ctx.req.method.toUpperCase())) return true; + + if (options.verifyFetchMetadata !== false) { + const site = ctx.req.headers.get("sec-fetch-site"); + if (site === "cross-site") return false; + } + + if (options.verifyOrigin !== false) { + const origin = ctx.req.headers.get("origin"); + if (origin) { + const trusted = new Set([ctx.url.origin, ...(options.trustedOrigins ?? [])]); + if (!trusted.has(origin)) return false; + } + } + const cookie = ctx.cookies.get(CSRF_COOKIE); const sent = ctx.req.headers.get(CSRF_HEADER) ?? (ctx.locals._csrf as string | undefined); return !!cookie && !!sent && timingSafeEqual(cookie, sent); } -/** - * Constant-time string comparison — the running time does not depend on where - * the first differing byte is, so an attacker can't time-probe the token. - */ +/** Constant-time string comparison. */ function timingSafeEqual(a: string, b: string): boolean { let diff = a.length ^ b.length; const max = Math.max(a.length, b.length); - for (let i = 0; i < max; i++) { - diff |= a.charCodeAt(i) ^ b.charCodeAt(i); - } + for (let i = 0; i < max; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); return diff === 0; } -/** Middleware that 403s unsafe requests with a missing/mismatched CSRF token. */ -export function csrfProtection(): Middleware { +/** Middleware that 403s unsafe requests with a missing/mismatched token. */ +export function csrfProtection(options: CsrfProtectionOptions = {}): Middleware { return (ctx, next) => - verifyCsrf(ctx) ? next() : new Response("Invalid CSRF token", { status: 403 }); + verifyCsrf(ctx, options) ? next() : new Response("Invalid CSRF token", { status: 403 }); } diff --git a/packages/core/src/headers.ts b/packages/core/src/headers.ts index 2e7b0c5f..8cb883f1 100644 --- a/packages/core/src/headers.ts +++ b/packages/core/src/headers.ts @@ -60,9 +60,23 @@ export interface TrustedTypesConfig { export type PermissionsPolicyConfig = Record; +export interface RequestLimitsConfig { + maxUrlLength?: number; + maxHeaderCount?: number; + maxHeaderBytes?: number; + maxQueryParameters?: number; + maxBodyBytes?: number; + timeoutMs?: number; + maxConcurrent?: number; + trustedHosts?: string[]; + fetchMetadata?: boolean; +} + export interface SecurityConfig { /** Set false to skip all framework security headers except explicitly enabled CORS. */ headers?: boolean; + /** Built-in request size, timeout, concurrency, host, and Fetch Metadata limits. */ + requestLimits?: RequestLimitsConfig; /** * Trust `X-Forwarded-Proto` / `X-Forwarded-Host` when building `ctx.url` — set * this when the app runs behind a TLS-terminating reverse proxy (nginx, the @@ -81,6 +95,12 @@ export interface SecurityConfig { frameOptions?: false | "DENY" | "SAMEORIGIN"; /** Defaults to "strict-origin-when-cross-origin". */ referrerPolicy?: false | string; + /** Defaults to "same-origin". */ + crossOriginResourcePolicy?: false | "same-origin" | "same-site" | "cross-origin"; + /** Isolate the origin in its own agent cluster. Defaults to true. */ + originAgentCluster?: boolean; + /** Disable speculative DNS prefetching. Defaults to true. */ + disableDnsPrefetch?: boolean; /** Defaults to a restrictive browser capability policy. */ permissionsPolicy?: false | PermissionsPolicyConfig; /** Extra static headers applied last. */ @@ -238,6 +258,11 @@ function applyBaseSecurityHeaders( const referrerPolicy = security?.referrerPolicy ?? "strict-origin-when-cross-origin"; if (referrerPolicy !== false) headers.set("Referrer-Policy", referrerPolicy); + const corp = security?.crossOriginResourcePolicy ?? "same-origin"; + if (corp !== false) headers.set("Cross-Origin-Resource-Policy", corp); + if (security?.originAgentCluster !== false) headers.set("Origin-Agent-Cluster", "?1"); + if (security?.disableDnsPrefetch !== false) headers.set("X-DNS-Prefetch-Control", "off"); + const configuredPermissions = security?.permissionsPolicy; const permissionsPolicy = configuredPermissions === false diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index be8517d3..2b9e6443 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,6 +14,7 @@ export { createContext, withContextHeaders } from "./context.ts"; export { escapeHtml, isSafeIslandName, isSafeRequestPath } from "./security.ts"; export { csrfToken, verifyCsrf, csrfProtection, CSRF_COOKIE, CSRF_HEADER } from "./csrf.ts"; +export type { CsrfProtectionOptions } from "./csrf.ts"; export { hashPassword, @@ -36,8 +37,23 @@ export type { RequestLoggerOptions, RequestRecord } from "./logging.ts"; export { TTLCache, cacheControl, withCacheControl, etag, notModified } from "./cache.ts"; export type { CacheControlOptions } from "./cache.ts"; -export { saveUpload, collectUploads, sanitizeFilename, UploadError } from "./uploads.ts"; -export type { SaveUploadOptions, SavedUpload } from "./uploads.ts"; +export { + saveUpload, + saveUploadSecure, + collectUploads, + sanitizeFilename, + randomUploadFilename, + secureDownloadHeaders, + UploadError, +} from "./uploads.ts"; +export type { + SaveUploadOptions, + SecureUploadOptions, + SavedUpload, + UploadInspectionResult, + UploadInspector, + UploadScanner, +} from "./uploads.ts"; export { streamResponse, sse } from "./stream.ts"; export type { StreamResponseInit, ServerSentEvent } from "./stream.ts"; @@ -63,6 +79,8 @@ export type { RealtimeConnectMeta, RealtimeBridge, RealtimeEnvelope, + RealtimeSecurityOptions, + RealtimeRegistryOptions, } from "./realtime.ts"; export type { Mode } from "./errors.ts"; @@ -81,6 +99,7 @@ export type { CspDirectiveValue, HstsConfig, PermissionsPolicyConfig, + RequestLimitsConfig, SecurityConfig, TrustedTypesConfig, } from "./headers.ts"; @@ -99,8 +118,9 @@ export type { SessionBackend, SessionEntry, AsyncSessionBackend, + SessionPolicy, } from "./storage.ts"; -export { setSessionBackend, loadSession } from "./storage.ts"; +export { setSessionBackend, setSessionPolicy, loadSession } from "./storage.ts"; export { Fragment, Html, jsx, jsxs, mustache } from "./jsx-runtime.ts"; export type { Component as JSXComponent, Props as JSXProps, Renderable } from "./jsx-runtime.ts"; @@ -132,7 +152,7 @@ export type { Span, SpanRecord, Tracer } from "./observability.ts"; export { defineFeatureFlags } from "./features.ts"; export type { FeatureFlags, FeatureRule, FeatureValue } from "./features.ts"; -export { checkPerformanceBudgets } from "./performance.ts"; +export { checkPerformanceBudgets, recommendedWebBudgets } from "./performance.ts"; export type { BudgetViolation, PerformanceBudgets, PerformanceMeasurement } from "./performance.ts"; export { problem, diff --git a/packages/core/src/performance.ts b/packages/core/src/performance.ts index a3edf091..c7766cbf 100644 --- a/packages/core/src/performance.ts +++ b/packages/core/src/performance.ts @@ -5,6 +5,20 @@ export interface PerformanceBudgets { imageBytes?: number; hydrationMs?: number; serverRenderMs?: number; + /** Largest Contentful Paint in milliseconds. Recommended <= 2500. */ + lcpMs?: number; + /** Interaction to Next Paint in milliseconds. Recommended <= 200. */ + inpMs?: number; + /** Cumulative Layout Shift score. Recommended <= 0.1. */ + cls?: number; + /** Time to First Byte in milliseconds. */ + ttfbMs?: number; + /** Longest main-thread task in milliseconds. Recommended <= 50. */ + longTaskMs?: number; + /** Number of client hydration boundaries on the route. */ + hydratedComponents?: number; + /** Total request count for the initial navigation. */ + requests?: number; } export interface PerformanceMeasurement { @@ -14,6 +28,13 @@ export interface PerformanceMeasurement { imageBytes?: number; hydrationMs?: number; serverRenderMs?: number; + lcpMs?: number; + inpMs?: number; + cls?: number; + ttfbMs?: number; + longTaskMs?: number; + hydratedComponents?: number; + requests?: number; } export interface BudgetViolation { @@ -23,6 +44,15 @@ export interface BudgetViolation { overBy: number; } +export const recommendedWebBudgets: Readonly = Object.freeze({ + lcpMs: 2500, + inpMs: 200, + cls: 0.1, + longTaskMs: 50, + routeJsBytes: 50 * 1024, + routeCssBytes: 25 * 1024, +}); + export function checkPerformanceBudgets( budgets: PerformanceBudgets, measurement: PerformanceMeasurement, diff --git a/packages/core/src/realtime.ts b/packages/core/src/realtime.ts index 4f9bfc8d..69c52ca0 100644 --- a/packages/core/src/realtime.ts +++ b/packages/core/src/realtime.ts @@ -100,7 +100,28 @@ export interface RoomAuthInfo { headers: Headers; } +export interface RealtimeSecurityOptions { + /** Maximum inbound or outbound serialized message size. Defaults to 64 KiB. */ + maxMessageBytes?: number; + /** Maximum messages accepted per connection per rolling second. Defaults to 30. */ + maxMessagesPerSecond?: number; + /** Maximum live connections in one room. Defaults to 1,000. */ + maxConnectionsPerRoom?: number; + /** Maximum connections for one authenticated user in a room. Defaults to 10. */ + maxConnectionsPerUser?: number; + /** Reject anonymous connections before onConnect. */ + requireUser?: boolean; + /** Maximum nested JSON depth. Defaults to 32. */ + maxJsonDepth?: number; + /** Optional message schema/authorization predicate. */ + validateMessage?(message: unknown, client: RoomClient): boolean | Promise; + /** Called when a connection is rejected or closed for a policy violation. */ + onViolation?(reason: string, client?: RoomClient): void; +} + export interface RoomHandlers> { + /** Per-room abuse and payload controls. */ + security?: RealtimeSecurityOptions; /** * Gate the connection BEFORE it is accepted. Return false to reject the * upgrade with 403 (e.g. `authorize: (info) => !!info.user` to require auth). @@ -138,6 +159,8 @@ export function isRoomDefinition(value: unknown): value is RoomDefinition { interface Conn { id: string; + messageWindowStartedAt: number; + messageCount: number; user?: string; data: Record; query: Record; @@ -179,6 +202,10 @@ export interface RealtimeBridge { publish(envelope: RealtimeEnvelope): void; } +export interface RealtimeRegistryOptions extends RealtimeSecurityOptions { + now?: () => number; +} + export interface RealtimeRegistry { open(socket: RawSocket, meta: RealtimeConnectMeta): void | Promise; message(socket: RawSocket, raw: string | Uint8Array): void | Promise; @@ -191,16 +218,59 @@ export interface RealtimeRegistry { size(): number; } +const DANGEROUS_REALTIME_KEYS = new Set(["__proto__", "prototype", "constructor"]); + +function assertRealtimePayload(value: unknown, maxDepth: number, depth = 0): void { + if (depth > maxDepth) throw new Error("Realtime payload nesting limit exceeded."); + if (!value || typeof value !== "object") return; + for (const [key, child] of Object.entries(value as Record)) { + if (DANGEROUS_REALTIME_KEYS.has(key)) throw new Error(`Dangerous realtime key '${key}'.`); + assertRealtimePayload(child, maxDepth, depth + 1); + } +} + +function byteLength(value: string | Uint8Array): number { + return typeof value === "string" ? new TextEncoder().encode(value).byteLength : value.byteLength; +} + function serialize(message: unknown): string { return typeof message === "string" ? message : JSON.stringify(message); } +function mergedRealtimeSecurity( + globalOptions: RealtimeRegistryOptions, + room: RoomDefinition, +): Required< + Pick< + RealtimeSecurityOptions, + | "maxMessageBytes" + | "maxMessagesPerSecond" + | "maxConnectionsPerRoom" + | "maxConnectionsPerUser" + | "requireUser" + | "maxJsonDepth" + > +> & + RealtimeSecurityOptions { + return { + maxMessageBytes: 64 * 1024, + maxMessagesPerSecond: 30, + maxConnectionsPerRoom: 1_000, + maxConnectionsPerUser: 10, + requireUser: false, + maxJsonDepth: 32, + ...globalOptions, + ...room.handlers.security, + }; +} + /** Create the registry that maps sockets ↔ rooms and drives room handlers. */ -export function createRealtimeRegistry(): RealtimeRegistry { +export function createRealtimeRegistry(options: RealtimeRegistryOptions = {}): RealtimeRegistry { const rooms = new Map(); const bySocket = new Map(); let bridge: RealtimeBridge | null = null; let applyingRemote = false; // true while delivering a peer envelope (no re-publish) + const now = options.now ?? Date.now; const publish = (envelope: RealtimeEnvelope): void => { if (bridge && !applyingRemote) bridge.publish(envelope); @@ -208,6 +278,17 @@ export function createRealtimeRegistry(): RealtimeRegistry { const send = (conn: Conn | undefined, payload: string): void => { if (!conn) return; + const room = rooms.get(conn.roomName); + const policy = room + ? mergedRealtimeSecurity(options, room.def) + : ({ maxMessageBytes: options.maxMessageBytes ?? 64 * 1024 } as ReturnType< + typeof mergedRealtimeSecurity + >); + if (byteLength(payload) > policy.maxMessageBytes) { + policy.onViolation?.("outbound-message-too-large", conn.client); + conn.socket.close(1009, "Message too large"); + return; + } try { conn.socket.send(payload); } catch { @@ -295,13 +376,31 @@ export function createRealtimeRegistry(): RealtimeRegistry { return { async open(socket, meta) { + const policy = mergedRealtimeSecurity(options, meta.def); + if (policy.requireUser && !meta.user) { + policy.onViolation?.("authentication-required"); + socket.close(1008, "Authentication required"); + return; + } let room = rooms.get(meta.room); if (!room) { room = { name: meta.room, state: {}, def: meta.def, conns: new Map(), users: new Map() }; rooms.set(meta.room, room); } + if (room.conns.size >= policy.maxConnectionsPerRoom) { + policy.onViolation?.("room-connection-limit"); + socket.close(1013, "Room is at capacity"); + return; + } + if (meta.user && (room.users.get(meta.user)?.size ?? 0) >= policy.maxConnectionsPerUser) { + policy.onViolation?.("user-connection-limit"); + socket.close(1008, "Too many connections"); + return; + } const conn: Conn = { id: randomId(), + messageWindowStartedAt: now(), + messageCount: 0, data: {}, query: meta.query ?? {}, socket, @@ -320,6 +419,23 @@ export function createRealtimeRegistry(): RealtimeRegistry { if (!conn) return; const room = rooms.get(conn.roomName); if (!room) return; + const policy = mergedRealtimeSecurity(options, room.def); + if (byteLength(raw) > policy.maxMessageBytes) { + policy.onViolation?.("inbound-message-too-large", conn.client); + socket.close(1009, "Message too large"); + return; + } + const timestamp = now(); + if (timestamp - conn.messageWindowStartedAt >= 1_000) { + conn.messageWindowStartedAt = timestamp; + conn.messageCount = 0; + } + conn.messageCount += 1; + if (conn.messageCount > policy.maxMessagesPerSecond) { + policy.onViolation?.("message-rate-limit", conn.client); + socket.close(1008, "Message rate exceeded"); + return; + } const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw); let message: unknown; try { @@ -327,6 +443,18 @@ export function createRealtimeRegistry(): RealtimeRegistry { } catch { message = text; } + try { + assertRealtimePayload(message, policy.maxJsonDepth); + } catch { + policy.onViolation?.("invalid-message-shape", conn.client); + socket.close(1008, "Invalid message"); + return; + } + if (policy.validateMessage && !(await policy.validateMessage(message, conn.client))) { + policy.onViolation?.("message-validation-failed", conn.client); + socket.close(1008, "Message rejected"); + return; + } await room.def.handlers.onMessage?.(conn.client, message); }, diff --git a/packages/core/src/storage.ts b/packages/core/src/storage.ts index e8b05e20..7a999a3b 100644 --- a/packages/core/src/storage.ts +++ b/packages/core/src/storage.ts @@ -37,15 +37,47 @@ export interface LocalStorageSnapshot { } const SESSION_COOKIE = "wrnexus.sid"; -/** Idle timeout: a session expires this long after its last access. */ -const SESSION_TTL_MS = 1000 * 60 * 60 * 24; // 24 hours -/** Run a background sweep after this many new sessions (bounds memory). */ +const SESSION_TTL_MS = 1000 * 60 * 60 * 24; +const SESSION_ABSOLUTE_TTL_MS = 1000 * 60 * 60 * 24 * 7; const SESSION_GC_EVERY = 500; +export interface SessionPolicy { + cookieName?: string; + idleTimeoutMs?: number; + absoluteTimeoutMs?: number; + sameSite?: NonNullable; + secure?: boolean; +} + +let sessionPolicy: Required< + Pick +> & + Pick = { + cookieName: SESSION_COOKIE, + idleTimeoutMs: SESSION_TTL_MS, + absoluteTimeoutMs: SESSION_ABSOLUTE_TTL_MS, + sameSite: "Lax", +}; + +export function setSessionPolicy(policy: SessionPolicy): void { + sessionPolicy = { + ...sessionPolicy, + ...policy, + idleTimeoutMs: Math.max(60_000, policy.idleTimeoutMs ?? sessionPolicy.idleTimeoutMs), + absoluteTimeoutMs: Math.max( + policy.idleTimeoutMs ?? sessionPolicy.idleTimeoutMs, + policy.absoluteTimeoutMs ?? sessionPolicy.absoluteTimeoutMs, + ), + }; +} + /** A stored session: its data plus an absolute expiry timestamp (ms). */ export interface SessionEntry { data: Record; expiresAt: number; + /** Creation time used for the absolute session lifetime. Optional for old backends. */ + createdAt?: number; + lastAccessAt?: number; } /** @@ -70,7 +102,13 @@ function createMemorySessionBackend(): SessionBackend { set: (id, entry) => void map.set(id, entry), delete: (id) => void map.delete(id), gc: (now) => { - for (const [key, entry] of map) if (entry.expiresAt <= now) map.delete(key); + for (const [key, entry] of map) { + if ( + entry.expiresAt <= now || + (entry.createdAt ?? now) + sessionPolicy.absoluteTimeoutMs <= now + ) + map.delete(key); + } }, }; } @@ -102,13 +140,26 @@ export interface AsyncSessionBackend { */ export function loadSession( backend: AsyncSessionBackend, - options: { ttlMs?: number } = {}, + options: { + ttlMs?: number; + absoluteTtlMs?: number; + cookieName?: string; + sameSite?: NonNullable; + secure?: boolean; + } = {}, ): Middleware { - const ttlMs = options.ttlMs ?? SESSION_TTL_MS; + const ttlMs = options.ttlMs ?? sessionPolicy.idleTimeoutMs; + const absoluteTtlMs = options.absoluteTtlMs ?? sessionPolicy.absoluteTimeoutMs; + const cookieName = options.cookieName ?? sessionPolicy.cookieName; return async (ctx: Context, next) => { - let id = ctx.cookies.get(SESSION_COOKIE); + let id = ctx.cookies.get(cookieName); let entry = id ? await backend.load(id) : undefined; - if (id && entry && entry.expiresAt <= Date.now()) { + if ( + id && + entry && + (entry.expiresAt <= Date.now() || + (entry.createdAt ?? Date.now()) + absoluteTtlMs <= Date.now()) + ) { await backend.destroy(id); entry = undefined; id = undefined; @@ -120,9 +171,16 @@ export function loadSession( const ensure = (): Record => { if (!id) { id = randomId(); - ctx.cookies.set(SESSION_COOKIE, id, sessionCookieOptions(ctx.url.protocol === "https:")); + ctx.cookies.set( + cookieName, + id, + sessionCookieOptions(options.secure ?? ctx.url.protocol === "https:", options.sameSite), + ); + } + if (!entry) { + const now = Date.now(); + entry = { data: {}, expiresAt: now + ttlMs, createdAt: now, lastAccessAt: now }; } - if (!entry) entry = { data: {}, expiresAt: Date.now() + ttlMs }; return entry.data; }; @@ -147,14 +205,22 @@ export function loadSession( const data = entry?.data ?? {}; if (id) destroys.add(id); id = randomId(); - entry = { data, expiresAt: Date.now() + ttlMs }; - ctx.cookies.set(SESSION_COOKIE, id, sessionCookieOptions(ctx.url.protocol === "https:")); + const now = Date.now(); + entry = { data, expiresAt: now + ttlMs, createdAt: now, lastAccessAt: now }; + ctx.cookies.set( + cookieName, + id, + sessionCookieOptions(options.secure ?? ctx.url.protocol === "https:", options.sameSite), + ); }, clear() { if (id) destroys.add(id); entry = undefined; id = undefined; - ctx.cookies.delete(SESSION_COOKIE, sessionCookieOptions(ctx.url.protocol === "https:")); + ctx.cookies.delete( + cookieName, + sessionCookieOptions(options.secure ?? ctx.url.protocol === "https:", options.sameSite), + ); }, }; @@ -164,6 +230,7 @@ export function loadSession( for (const gone of destroys) if (gone !== id) await backend.destroy(gone); if (id && entry) { entry.expiresAt = Date.now() + ttlMs; + entry.lastAccessAt = Date.now(); await backend.save(id, entry); } } @@ -176,11 +243,14 @@ const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/; function readSessionEntry(id: string): SessionEntry | undefined { const entry = sessionBackend.get(id); if (!entry) return undefined; - if (entry.expiresAt <= Date.now()) { + const now = Date.now(); + if (entry.expiresAt <= now || (entry.createdAt ?? now) + sessionPolicy.absoluteTimeoutMs <= now) { sessionBackend.delete(id); return undefined; } - entry.expiresAt = Date.now() + SESSION_TTL_MS; // sliding idle expiry + entry.lastAccessAt = now; + entry.createdAt ??= now; + entry.expiresAt = now + sessionPolicy.idleTimeoutMs; // sliding idle expiry sessionBackend.set(id, entry); // persist the slide (matters for external backends) return entry; } @@ -223,8 +293,8 @@ export function createCookieStore(req: Request): CookieStore { export function createSessionStore( cookies: CookieStore, req: Request, - cookieName = SESSION_COOKIE, - secure = new URL(req.url).protocol === "https:", + cookieName = sessionPolicy.cookieName, + secure = sessionPolicy.secure ?? new URL(req.url).protocol === "https:", ): SessionStore { let id = cookies.get(cookieName); let entry = id ? readSessionEntry(id) : undefined; @@ -245,7 +315,13 @@ export function createSessionStore( sessionsSinceGc = 0; sessionBackend.gc?.(Date.now()); } - entry = { data: {}, expiresAt: Date.now() + SESSION_TTL_MS }; + const now = Date.now(); + entry = { + data: {}, + expiresAt: now + sessionPolicy.idleTimeoutMs, + createdAt: now, + lastAccessAt: now, + }; sessionBackend.set(id, entry); } return entry.data; @@ -278,7 +354,13 @@ export function createSessionStore( const data = entry?.data ?? {}; if (id) sessionBackend.delete(id); id = randomId(); - entry = { data, expiresAt: Date.now() + SESSION_TTL_MS }; + const now = Date.now(); + entry = { + data, + expiresAt: now + sessionPolicy.idleTimeoutMs, + createdAt: now, + lastAccessAt: now, + }; sessionBackend.set(id, entry); cookies.set(cookieName, id, sessionCookieOptions(secure)); }, @@ -341,11 +423,14 @@ function serializeCookie(name: string, value: string, options: CookieOptions): s return parts.join("; "); } -function sessionCookieOptions(secure: boolean): CookieOptions { +function sessionCookieOptions( + secure: boolean, + sameSite: NonNullable = sessionPolicy.sameSite, +): CookieOptions { return { httpOnly: true, path: "/", - sameSite: "Lax", + sameSite, secure, }; } @@ -358,6 +443,7 @@ function parseLocalStorageHeader(header: string | null): Record const out: Record = {}; for (const [key, value] of Object.entries(parsed)) { + if (key === "__proto__" || key === "prototype" || key === "constructor") continue; if (typeof value === "string") out[key] = value; } return out; diff --git a/packages/core/src/uploads.ts b/packages/core/src/uploads.ts index 4bea22ec..7098392b 100644 --- a/packages/core/src/uploads.ts +++ b/packages/core/src/uploads.ts @@ -1,19 +1,41 @@ /** - * File upload helpers. Bun parses `multipart/form-data` natively via - * `Request.formData()`, yielding web `File` objects; these helpers validate and - * persist them safely (size/type limits, filename sanitisation to prevent path - * traversal). + * File upload helpers. The legacy `saveUpload` keeps the original sanitized + * filename for compatibility. New applications should use `saveUploadSecure`, + * which stores a random name and supports content inspection/scanning hooks. */ export class UploadError extends Error { - constructor(message: string) { + readonly code: string; + constructor(message: string, code = "WRN-UPLOAD-REJECTED") { super(message); this.name = "UploadError"; + this.code = code; } } +export interface UploadInspectionResult { + allowed: boolean; + detectedType?: string; + reason?: string; +} + +export type UploadInspector = (input: { + file: File; + bytes: Uint8Array; + filename: string; +}) => UploadInspectionResult | Promise; + +export type UploadScanner = (input: { + file: File; + bytes: Uint8Array; + filename: string; +}) => + | boolean + | { clean: boolean; reason?: string } + | Promise; + export interface SaveUploadOptions { - /** Destination directory. */ + /** Destination directory. Keep this outside the public web root. */ dir: string; /** Reject files larger than this many bytes. */ maxBytes?: number; @@ -21,6 +43,21 @@ export interface SaveUploadOptions { allowedTypes?: string[]; /** Choose the stored filename. Default: the sanitised original name. */ filename?: (file: File) => string; + /** Content/magic-byte inspection hook. */ + inspect?: UploadInspector; + /** Malware scanning hook. */ + scan?: UploadScanner; + /** Called after validation but before persistence. */ + beforeSave?: (input: { file: File; bytes: Uint8Array; filename: string }) => void | Promise; +} + +export interface SecureUploadOptions extends Omit { + /** Preserve the original sanitized name instead of a random server name. */ + preserveOriginalName?: boolean; + /** Optional custom secure filename generator. */ + filename?: (file: File) => string; + /** Preserve a conservative extension on random filenames. Defaults to true. */ + preserveExtension?: boolean; } export interface SavedUpload { @@ -28,51 +65,152 @@ export interface SavedUpload { filename: string; size: number; type: string; + detectedType?: string; } /** All `File` values in a parsed form, with their field names. */ -export function collectUploads(form: FormData): { field: string; file: File }[] { +export function collectUploads( + form: FormData, + options: { maxFiles?: number; maxTotalBytes?: number } = {}, +): { field: string; file: File }[] { const out: { field: string; file: File }[] = []; + let totalBytes = 0; for (const [field, value] of form) { - if (value instanceof File && value.size > 0) out.push({ field, file: value }); + if (!(value instanceof File) || value.size <= 0) continue; + out.push({ field, file: value }); + totalBytes += value.size; + if (options.maxFiles !== undefined && out.length > options.maxFiles) { + throw new UploadError( + `Upload contains more than ${options.maxFiles} files.`, + "WRN-UPLOAD-FILE-COUNT", + ); + } + if (options.maxTotalBytes !== undefined && totalBytes > options.maxTotalBytes) { + throw new UploadError( + `Upload exceeds the ${options.maxTotalBytes}-byte aggregate limit.`, + "WRN-UPLOAD-TOTAL-SIZE", + ); + } } return out; } -/** Validate and write one uploaded file to disk. Throws `UploadError` on reject. */ +/** Validate and write one uploaded file using a compatibility filename policy. */ export async function saveUpload(file: File, options: SaveUploadOptions): Promise { + return persistUpload( + file, + options, + sanitizeFilename(options.filename ? options.filename(file) : file.name || "upload"), + ); +} + +/** Store an upload under a random server-generated name by default. */ +export async function saveUploadSecure( + file: File, + options: SecureUploadOptions, +): Promise { + const requested = options.filename?.(file); + const filename = requested + ? sanitizeFilename(requested) + : options.preserveOriginalName + ? sanitizeFilename(file.name || "upload") + : randomUploadFilename(file.name, options.preserveExtension !== false); + return persistUpload(file, options, filename); +} + +async function persistUpload( + file: File, + options: SaveUploadOptions, + filename: string, +): Promise { if (options.maxBytes !== undefined && file.size > options.maxBytes) { - throw new UploadError(`File "${file.name}" exceeds the ${options.maxBytes}-byte limit`); + throw new UploadError( + `File "${file.name}" exceeds the ${options.maxBytes}-byte limit`, + "WRN-UPLOAD-SIZE", + ); } if (options.allowedTypes && !isAllowed(file, options.allowedTypes)) { - throw new UploadError(`File type not allowed: ${file.type || file.name || "unknown"}`); + throw new UploadError( + `File type not allowed: ${file.type || file.name || "unknown"}`, + "WRN-UPLOAD-TYPE", + ); } - const filename = sanitizeFilename( - options.filename ? options.filename(file) : file.name || "upload", - ); + const bytes = new Uint8Array(await file.arrayBuffer()); + let detectedType: string | undefined; + if (options.inspect) { + const result = await options.inspect({ file, bytes, filename }); + if (!result.allowed) { + throw new UploadError(result.reason ?? "File content is not allowed.", "WRN-UPLOAD-CONTENT"); + } + detectedType = result.detectedType; + } + if (options.scan) { + const result = await options.scan({ file, bytes, filename }); + const clean = typeof result === "boolean" ? result : result.clean; + if (!clean) { + throw new UploadError( + typeof result === "boolean" + ? "File failed malware scanning." + : (result.reason ?? "File failed malware scanning."), + "WRN-UPLOAD-MALWARE", + ); + } + } + await options.beforeSave?.({ file, bytes, filename }); + const path = `${options.dir.replace(/[/\\]+$/, "")}/${filename}`; - await Bun.write(path, file); - return { path, filename, size: file.size, type: file.type }; + await Bun.write(path, bytes); + return { + path, + filename, + size: file.size, + type: file.type, + ...(detectedType ? { detectedType } : {}), + }; } function isAllowed(file: File, allowed: string[]): boolean { const type = (file.type || "").toLowerCase(); const name = (file.name || "").toLowerCase(); return allowed.some((entry) => { - const e = entry.toLowerCase(); - return e.startsWith(".") ? name.endsWith(e) : type === e; + const candidate = entry.toLowerCase(); + if (candidate.startsWith(".")) return name.endsWith(candidate); + if (candidate.endsWith("/*")) return type.startsWith(candidate.slice(0, -1)); + return type === candidate; }); } /** Strip directory separators, traversal, and control chars from a filename. */ export function sanitizeFilename(name: string): string { const base = name - .replace(/[/\\]+/g, "_") // path separators - .replace(/\.\.+/g, ".") // collapse traversal dots + .replace(/[/\\]+/g, "_") + .replace(/\.\.+/g, ".") // eslint-disable-next-line no-control-regex -- intentionally stripping control chars - .replace(/[\x00-\x1f<>:"|?*]/g, "") // control + illegal chars - .replace(/^\.+/, "") // no leading dots + .replace(/[\x00-\x1f<>:"|?*]/g, "") + .replace(/^\.+/, "") .trim(); return base.length > 0 ? base.slice(0, 255) : "upload"; } + +export function randomUploadFilename(originalName = "", preserveExtension = true): string { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + const id = [...bytes].map((value) => value.toString(16).padStart(2, "0")).join(""); + if (!preserveExtension) return id; + const match = /(?:^|\.)([A-Za-z0-9]{1,10})$/.exec(originalName); + return match ? `${id}.${match[1]!.toLowerCase()}` : id; +} + +export function secureDownloadHeaders( + filename: string, + type = "application/octet-stream", +): Headers { + const safe = sanitizeFilename(filename).replace(/["\\]/g, "_"); + return new Headers({ + "content-type": type, + "content-disposition": `attachment; filename="${safe}"`, + "x-content-type-options": "nosniff", + "cache-control": "private, no-store", + }); +} diff --git a/packages/core/test/realtime.test.ts b/packages/core/test/realtime.test.ts index 5d7aa4b3..57c222ec 100644 --- a/packages/core/test/realtime.test.ts +++ b/packages/core/test/realtime.test.ts @@ -159,3 +159,46 @@ test("room.state and count() track the live room", async () => { expect(a.sent[0]).toMatchObject({ online: 1, hits: 1 }); expect(b.sent[0]).toMatchObject({ online: 2, hits: 2 }); }); + +test("realtime security enforces authentication, message size, rate, and safe shapes", async () => { + const violations: string[] = []; + const def = defineRoom({ + security: { + requireUser: true, + maxMessageBytes: 64, + maxMessagesPerSecond: 1, + onViolation(reason) { + violations.push(reason); + }, + }, + }); + const now = { value: 0 }; + const reg = createRealtimeRegistry({ now: () => now.value }); + const anonymousClosures: Array<[number | undefined, string | undefined]> = []; + const anonymous: RawSocket = { + send() {}, + close(code, reason) { + anonymousClosures.push([code, reason]); + }, + }; + await reg.open(anonymous, { room: "/secure", def }); + expect(anonymousClosures[0]).toEqual([1008, "Authentication required"]); + + const closures: Array<[number | undefined, string | undefined]> = []; + const socket: RawSocket = { + send() {}, + close(code, reason) { + closures.push([code, reason]); + }, + }; + await reg.open(socket, { room: "/secure", def, user: "u1" }); + await reg.message(socket, JSON.stringify({ ok: true })); + await reg.message(socket, JSON.stringify({ ok: true })); + expect(closures.at(-1)).toEqual([1008, "Message rate exceeded"]); + expect(violations).toContain("message-rate-limit"); + + now.value = 2_000; + const unsafe = '{"constructor":{"prototype":{"admin":true}}}'; + await reg.message(socket, unsafe); + expect(violations).toContain("invalid-message-shape"); +}); diff --git a/packages/csr/package.json b/packages/csr/package.json index 53c1901b..bc09fb4a 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index d46ad61d..d2409a8f 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -352,8 +352,22 @@ export const REACTIVE_RUNTIME = String.raw` }, }; } + function sanitizeReactiveUrl(value) { + var raw = String(value == null ? "" : value); + var compact = raw.trim().replace(/[\u0000-\u0020]+/g, "").toLowerCase(); + if (/^(?:javascript|vbscript|file):/.test(compact)) return "about:blank"; + if (/^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(compact)) return "about:blank"; + return raw; + } + function applyReactiveAttribute(node, name, value) { var lowerName = String(name || "").toLowerCase(); + if ( + value != null && + ["href", "src", "action", "formaction", "poster", "cite", "background", "xlink:href"].indexOf(lowerName) !== -1 + ) { + value = sanitizeReactiveUrl(value); + } if ( lowerName === "value" && @@ -2030,16 +2044,29 @@ export const REACTIVE_RUNTIME = String.raw` function hydrate() { if (element.__wrnexusScope || !element.isConnected) return; + var started = performance.now ? performance.now() : Date.now(); + function complete(module) { + if (element.__wrnexusScope || !element.isConnected) return; + if (module) element.__wrnexusClientModule = module; + setupScope(element); + var ended = performance.now ? performance.now() : Date.now(); + try { + element.dispatchEvent(new CustomEvent("wrnexus:hydrated", { + bubbles: true, + detail: { + id: element.getAttribute("data-wrn-hydration") || null, + strategy: strategy, + durationMs: Math.max(0, ended - started), + }, + })); + } catch (_) {} + } var moduleUrl = element.getAttribute("data-wrn-client-module"); if (!moduleUrl || moduleUrl === "__WRNEXUS_CLIENT_MODULE__") { - setupScope(element); + complete(null); return; } - loadClientModule(element).then(function (module) { - if (element.__wrnexusScope || !element.isConnected) return; - element.__wrnexusClientModule = module; - setupScope(element); - }); + loadClientModule(element).then(complete); } if (strategy === "load") { @@ -2744,7 +2771,7 @@ export const REACTIVE_RUNTIME = String.raw` credentials: "same-origin", headers: { "content-type": "application/json", - "x-wrnexus-csrf": csrf ? csrf.getAttribute("content") || "" : "", + "x-csrf-token": csrf ? csrf.getAttribute("content") || "" : "", }, body: JSON.stringify({ component: component, function: functionName, args: args || [] }), }).then(function (response) { diff --git a/packages/csr/src/server-client.ts b/packages/csr/src/server-client.ts index 62d33c8f..9f097a79 100644 --- a/packages/csr/src/server-client.ts +++ b/packages/csr/src/server-client.ts @@ -17,7 +17,7 @@ export class WrnServerCallError extends Error { function csrfFromCookie(): string | undefined { if (typeof document === "undefined") return undefined; - const raw = /(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(document.cookie)?.[1]; + const raw = /(?:^|;\s*)wire-csrf=([^;]+)/.exec(document.cookie)?.[1]; return raw ? decodeURIComponent(raw) : undefined; } @@ -34,7 +34,7 @@ export async function callServerFunction( signal: options.signal, headers: { "content-type": "application/json", - ...(csrfToken ? { "x-wrnexus-csrf": csrfToken } : {}), + ...(csrfToken ? { "x-csrf-token": csrfToken } : {}), ...options.headers, }, body: JSON.stringify({ component, function: functionName, args }), diff --git a/packages/csr/test/nav.test.ts b/packages/csr/test/nav.test.ts index 662485f7..bf979ded 100644 --- a/packages/csr/test/nav.test.ts +++ b/packages/csr/test/nav.test.ts @@ -73,11 +73,15 @@ test("ignores cross-origin links (full navigation)", async () => { test("ignores modified clicks so new-tab still works", async () => { install(``); - win.document - .getElementById("lnk") - .dispatchEvent( - new win.MouseEvent("click", { bubbles: true, cancelable: true, button: 0, metaKey: true }), - ); + const MouseEventConstructor = (win as unknown as { MouseEvent: typeof MouseEvent }).MouseEvent; + win.document.getElementById("lnk").dispatchEvent( + new MouseEventConstructor("click", { + bubbles: true, + cancelable: true, + button: 0, + metaKey: true, + }), + ); await flush(); expect(fetchCalls.length).toBe(0); }); diff --git a/packages/csr/test/reactive.test.ts b/packages/csr/test/reactive.test.ts index 256a858f..0f6079bc 100644 --- a/packages/csr/test/reactive.test.ts +++ b/packages/csr/test/reactive.test.ts @@ -73,7 +73,10 @@ test("component functions support formatted multiline assignments and ternaries" increment.click(); expect(increment.textContent).toBe("1"); - increment.dispatchEvent(new win.MouseEvent("click", { shiftKey: true }) as unknown as Event); + const MouseEventConstructor = (win as unknown as { MouseEvent: typeof MouseEvent }).MouseEvent; + increment.dispatchEvent( + new MouseEventConstructor("click", { shiftKey: true }) as unknown as Event, + ); expect(increment.textContent).toBe("11"); (win.document.getElementById("normalize") as unknown as HTMLElement).click(); diff --git a/packages/db/package.json b/packages/db/package.json index 27a0f717..84799573 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index ab0ea444..1f845687 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -38,3 +38,6 @@ export { paginate, loadRelated } from "./query.ts"; export type { Paginated, PageOptions, RelationOptions } from "./query.ts"; export { cursorPaginate, optimisticUpdate, tenantScope, softDeleteClause } from "./advanced.ts"; export type { CursorPage, CursorPageOptions } from "./advanced.ts"; + +export { instrumentDb, queryOperation } from "./performance.ts"; +export type { QueryIssue, QueryPolicy, QueryRecord } from "./performance.ts"; diff --git a/packages/db/src/performance.ts b/packages/db/src/performance.ts new file mode 100644 index 00000000..1b9084ba --- /dev/null +++ b/packages/db/src/performance.ts @@ -0,0 +1,179 @@ +import type { Db, ExecResult, Row } from "./driver.ts"; +import type { Model } from "./schema.ts"; + +export interface QueryRecord { + sql: string; + paramsCount: number; + durationMs: number; + rowCount?: number; + operation: "all" | "one" | "exec"; + duplicateCount: number; +} + +export interface QueryIssue { + code: string; + severity: "error" | "warning" | "info"; + message: string; + sql: string; +} + +export interface QueryPolicy { + slowQueryMs?: number; + timeoutMs?: number; + maxRows?: number; + duplicateWarningCount?: number; + warnSelectStar?: boolean; + warnUnboundedSelect?: boolean; + onQuery?: (record: QueryRecord) => void | Promise; + onIssue?: (issue: QueryIssue) => void | Promise; +} + +function normalizedSql(sql: string): string { + return sql + .replace(/--.*$/gm, " ") + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function operationName(sql: string): string { + return normalizedSql(sql).split(" ")[0]?.toUpperCase() ?? "UNKNOWN"; +} + +async function withTimeout(promise: Promise, timeoutMs: number, sql: string): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_resolve, reject) => { + timer = setTimeout( + () => + reject( + new Error(`WRN-DB-TIMEOUT: query exceeded ${timeoutMs} ms: ${sql.slice(0, 120)}`), + ), + timeoutMs, + ); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +export function instrumentDb(db: Db, policy: QueryPolicy = {}): Db { + const counts = new Map(); + const slowQueryMs = policy.slowQueryMs ?? 100; + const timeoutMs = policy.timeoutMs ?? 30_000; + const maxRows = policy.maxRows ?? 10_000; + const duplicateWarningCount = policy.duplicateWarningCount ?? 5; + + const inspect = async (sql: string): Promise => { + const normalized = normalizedSql(sql); + if (policy.warnSelectStar !== false && /^SELECT\s+\*/i.test(normalized)) { + await policy.onIssue?.({ + code: "WRN-DB-SELECT-STAR", + severity: "warning", + message: "Avoid SELECT * in production queries.", + sql, + }); + } + if ( + policy.warnUnboundedSelect !== false && + /^SELECT\b/i.test(normalized) && + !/\bLIMIT\b/i.test(normalized) && + !/\bCOUNT\s*\(/i.test(normalized) + ) { + await policy.onIssue?.({ + code: "WRN-DB-UNBOUNDED-SELECT", + severity: "warning", + message: "SELECT query has no LIMIT. Prefer cursor pagination for large datasets.", + sql, + }); + } + }; + + const record = async ( + sql: string, + params: unknown[], + operation: QueryRecord["operation"], + durationMs: number, + rowCount?: number, + ): Promise => { + const key = `${operation}:${normalizedSql(sql)}:${JSON.stringify(params)}`; + const duplicateCount = (counts.get(key) ?? 0) + 1; + counts.set(key, duplicateCount); + const queryRecord: QueryRecord = { + sql, + paramsCount: params.length, + durationMs, + ...(rowCount === undefined ? {} : { rowCount }), + operation, + duplicateCount, + }; + await policy.onQuery?.(queryRecord); + if (durationMs >= slowQueryMs) { + await policy.onIssue?.({ + code: "WRN-DB-SLOW-QUERY", + severity: durationMs >= slowQueryMs * 5 ? "error" : "warning", + message: `Query took ${durationMs.toFixed(2)} ms.`, + sql, + }); + } + if (duplicateCount === duplicateWarningCount) { + await policy.onIssue?.({ + code: "WRN-DB-DUPLICATE-QUERY", + severity: "warning", + message: `The same query ran ${duplicateCount} times in one request scope (possible N+1).`, + sql, + }); + } + }; + + const wrapped: Db = { + driver: db.driver, + async all(sql: string, params: unknown[] = [], model?: Model): Promise { + await inspect(sql); + const started = performance.now(); + const rows = await withTimeout(db.all(sql, params, model), timeoutMs, sql); + const duration = performance.now() - started; + if (rows.length > maxRows) { + await policy.onIssue?.({ + code: "WRN-DB-MAX-ROWS", + severity: "error", + message: `Query returned ${rows.length} rows; maximum is ${maxRows}.`, + sql, + }); + throw new Error(`WRN-DB-MAX-ROWS: query returned ${rows.length} rows.`); + } + await record(sql, params, "all", duration, rows.length); + return rows; + }, + async one(sql: string, params: unknown[] = [], model?: Model): Promise { + await inspect(sql); + const started = performance.now(); + const row = await withTimeout(db.one(sql, params, model), timeoutMs, sql); + await record(sql, params, "one", performance.now() - started, row ? 1 : 0); + return row; + }, + async exec(sql: string, params: unknown[] = []): Promise { + const started = performance.now(); + const result = await withTimeout(db.exec(sql, params), timeoutMs, sql); + await record(sql, params, "exec", performance.now() - started, result.changes); + return result; + }, + tx(fn: (transaction: Db) => Promise): Promise { + return db.tx((transaction) => fn(instrumentDb(transaction, policy))); + }, + createTable(model) { + return db.createTable(model); + }, + close() { + return db.close(); + }, + }; + return wrapped; +} + +export function queryOperation(sql: string): string { + return operationName(sql); +} diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 358c4e43..d2b4492e 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { @@ -22,6 +22,8 @@ "@wrnexus/pubsub": "workspace:*", "@wrnexus/uploader": "workspace:*", "@wrnexus/plugin": "workspace:*", - "@wrnexus/store": "workspace:*" + "@wrnexus/store": "workspace:*", + "@wrnexus/security": "workspace:*", + "@wrnexus/observability": "workspace:*" } } diff --git a/packages/dev-server/src/gateway.ts b/packages/dev-server/src/gateway.ts index 41c5e8a8..37181f40 100644 --- a/packages/dev-server/src/gateway.ts +++ b/packages/dev-server/src/gateway.ts @@ -53,6 +53,23 @@ export interface GatewayApp { } /** Gateway-wide security controls, enforced for every app. */ +export interface GatewayRequestLimits { + maxUrlLength?: number; + maxHeaderCount?: number; + maxHeaderBytes?: number; + maxQueryParameters?: number; + maxBodyBytes?: number; + timeoutMs?: number; + maxConcurrent?: number; + fetchMetadata?: boolean; +} + +export interface GatewayWebSocketSecurity { + maxMessageBytes?: number; + maxQueuedMessages?: number; + allowedOrigins?: string[]; +} + export interface GatewaySecurity { /** Reject requests whose Host matches no app (404) instead of routing to the first. */ trustedHostsOnly?: boolean; @@ -64,6 +81,10 @@ export interface GatewaySecurity { forwardedHeaders?: boolean; /** Log each request (host → app, method, path, status). */ accessLog?: boolean; + /** URL, header, body, timeout, concurrency, and Fetch Metadata limits. */ + requestLimits?: GatewayRequestLimits; + /** WebSocket origin, payload, and pre-connect queue limits. */ + websocket?: GatewayWebSocketSecurity; } export interface GatewayOptions { @@ -93,6 +114,8 @@ interface WsBridge { path: string; backend?: WebSocket; queue: Array; + maxMessageBytes: number; + maxQueuedMessages: number; } /** Decide whether a gateway child should be relaunched after it exits. */ @@ -121,6 +144,37 @@ function makeRateLimiter(max: number, windowMs: number) { }; } +function requestHeaderBytes(headers: Headers): number { + let total = 0; + headers.forEach((value, name) => { + total += name.length + value.length + 4; + }); + return total; +} + +function requestMessageBytes(value: string | ArrayBuffer | ArrayBufferView): number { + if (typeof value === "string") return new TextEncoder().encode(value).byteLength; + return value instanceof ArrayBuffer ? value.byteLength : value.byteLength; +} + +function gatewayWebSocketOriginAllowed( + req: Request, + target: Target, + configured: string[], +): boolean { + const origin = req.headers.get("origin"); + if (!origin) return true; + let parsed: URL; + try { + parsed = new URL(origin); + } catch { + return false; + } + if (configured.includes(origin)) return true; + if (target.publicOrigin && origin === new URL(target.publicOrigin).origin) return true; + return target.domains.some((domain) => parsed.host.toLowerCase() === domain.toLowerCase()); +} + /** Constant-time-ish string compare. */ function timingSafeEqual(a: string, b: string): boolean { if (a.length !== b.length) return false; @@ -486,16 +540,48 @@ export async function startGateway(opts: GatewayOptions): Promise Date.now(); + const limits = sec.requestLimits ?? {}; + const websocketSecurity = sec.websocket ?? {}; + const maxBodyBytes = limits.maxBodyBytes ?? 10 * 1024 * 1024; + const maxConcurrent = limits.maxConcurrent ?? 1_000; + let activeRequests = 0; const createGatewayServer = () => Bun.serve({ port, hostname, development: mode === "development", - maxRequestBodySize: 50 * 1024 * 1024, + maxRequestBodySize: maxBodyBytes, async fetch(req, srv) { const url = new URL(req.url); const ip = srv.requestIP(req)?.address ?? ""; + if (req.url.length > (limits.maxUrlLength ?? 8_192)) { + return new Response("URI Too Long", { status: 414 }); + } + if ([...req.headers].length > (limits.maxHeaderCount ?? 100)) { + return new Response("Too Many Headers", { status: 431 }); + } + if (requestHeaderBytes(req.headers) > (limits.maxHeaderBytes ?? 32 * 1024)) { + return new Response("Request Headers Too Large", { status: 431 }); + } + if ([...url.searchParams].length > (limits.maxQueryParameters ?? 100)) { + return new Response("Too Many Query Parameters", { status: 400 }); + } + const declaredLength = Number(req.headers.get("content-length") ?? 0); + if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) { + return new Response("Payload Too Large", { status: 413 }); + } + if (limits.fetchMetadata !== false) { + const site = req.headers.get("sec-fetch-site"); + const mode = req.headers.get("sec-fetch-mode"); + if ( + site === "cross-site" && + !["GET", "HEAD", "OPTIONS"].includes(req.method) && + mode !== "cors" + ) { + return new Response("Cross-site request denied", { status: 403 }); + } + } // Health/status endpoint (not proxied). if (url.pathname === "/__gateway/health") { @@ -539,8 +625,17 @@ export async function startGateway(opts: GatewayOptions): Promise= maxConcurrent) { + return new Response("Gateway overloaded", { + status: 503, + headers: { "retry-after": "1" }, + }); + } + activeRequests += 1; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), limits.timeoutMs ?? 30_000); try { res = await fetch(target.origin + url.pathname + url.search, { method: req.method, headers, body, redirect: "manual", + signal: controller.signal, }); } catch (error) { console.error( `[wrnexus] gateway proxy error: ${req.method} ${url.pathname} → ${target.name} (${target.origin})`, error instanceof Error ? (error.stack ?? error.message) : error, ); - res = new Response(`Gateway: app '${target.name}' is unavailable.`, { status: 502 }); + res = new Response(`Gateway: app '${target.name}' is unavailable.`, { + status: error instanceof DOMException && error.name === "AbortError" ? 504 : 502, + }); + } finally { + clearTimeout(timeout); + activeRequests -= 1; } const diagnostic = internalError(res); if (res.status >= 500 && diagnostic) { @@ -589,6 +699,21 @@ export async function startGateway(opts: GatewayOptions): Promise { const data = event.data; + const size = + typeof data === "string" + ? new TextEncoder().encode(data).byteLength + : data instanceof Blob + ? data.size + : data instanceof ArrayBuffer + ? data.byteLength + : ArrayBuffer.isView(data) + ? data.byteLength + : 0; + if (size > ws.data.maxMessageBytes) { + ws.close(1009, "Message too large"); + backend.close(1009, "Message too large"); + return; + } if (typeof data === "string") { ws.send(data); @@ -619,6 +744,11 @@ export async function startGateway(opts: GatewayOptions): Promise ws.close()); }, message(ws, message) { + if (requestMessageBytes(message) > ws.data.maxMessageBytes) { + ws.close(1009, "Message too large"); + ws.data.backend?.close(1009, "Message too large"); + return; + } let data: string | ArrayBuffer; if (typeof message === "string") { @@ -638,8 +768,11 @@ export async function startGateway(opts: GatewayOptions): Promise { const url = new URL(request.url); const origin = request.headers.get("origin"); if (origin && origin !== url.origin) return false; - const cookieToken = /(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec( - request.headers.get("cookie") ?? "", - )?.[1]; - return ( - !cookieToken || - decodeURIComponent(cookieToken) === (request.headers.get("x-wrnexus-csrf") ?? "") - ); + const cookieHeader = request.headers.get("cookie") ?? ""; + const cookieToken = + /(?:^|;\s*)wire-csrf=([^;]+)/.exec(cookieHeader)?.[1] ?? + /(?:^|;\s*)wrnexus_csrf=([^;]+)/.exec(cookieHeader)?.[1]; + const headerToken = + request.headers.get("x-csrf-token") ?? request.headers.get("x-wrnexus-csrf") ?? ""; + return !cookieToken || decodeURIComponent(cookieToken) === headerToken; }, }); diff --git a/packages/dev-server/src/runtime.ts b/packages/dev-server/src/runtime.ts index 1fbcbd58..e1d559a2 100644 --- a/packages/dev-server/src/runtime.ts +++ b/packages/dev-server/src/runtime.ts @@ -1,3 +1,4 @@ +import { brotliCompressSync, constants as zlibConstants } from "node:zlib"; /** * Shared request runtime used by BOTH the dev server and the production server. * @@ -35,6 +36,13 @@ import { type SeoConfig, type TFunction, } from "@wrnexus/core"; +import { requestHardening } from "@wrnexus/security"; +import { + createWebVitalsHandler, + defaultMetrics, + metricsMiddleware, + webVitalsClient, +} from "@wrnexus/observability"; import type { Router } from "@wrnexus/router"; import { renderDocument, type RenderScript, type ScriptAsset } from "@wrnexus/ssr"; import { @@ -142,7 +150,7 @@ export interface RuntimeDeps { /** Package browser runtimes resolved by the plugin system. */ clientRuntimes?: ClientRuntimeDefinition[]; /** Page navigation strategy. `document` disables same-origin link interception. */ - navigation?: { mode?: "client" | "document" }; + navigation?: { mode?: "auto" | "client" | "document" }; /** Raw HTML appended to every page head (e.g. CDN framework links). */ head?: string; /** Global SEO defaults. */ @@ -216,7 +224,12 @@ function tenantIdentityFromConfig( function frameworkMiddleware(deps: RuntimeDeps): Middleware[] { const middleware: Middleware[] = []; + if (deps.security?.requestLimits) { + middleware.push(requestHardening(deps.security.requestLimits)); + } + if (deps.observability && deps.observability.enabled !== false) { + middleware.push(metricsMiddleware({ registry: defaultMetrics, includePath: false })); middleware.push( tracingMiddleware(undefined, { sampleRate: deps.observability.sampleRate, @@ -855,6 +868,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers { const pwaEnabled = deps.pwa !== false && deps.pwa?.enabled !== false; const pwaConfig: PwaConfig = deps.pwa && typeof deps.pwa === "object" ? deps.pwa : {}; const pwaServiceWorkerEnabled = pwaEnabled && pwaConfig.serviceWorker !== false; + const webVitalsEnabled = + deps.observability?.enabled !== false && deps.observability?.webVitals === true; + const webVitalsEndpoint = deps.observability?.webVitalsEndpoint ?? "/__wrnexus/metrics/vitals"; + const webVitalsHandler = createWebVitalsHandler({ registry: defaultMetrics }); const configuredPermissions = deps.security?.permissionsPolicy; const runtimeSecurity: SecurityConfig | undefined = @@ -883,6 +900,27 @@ export function createHandlers(deps: RuntimeDeps): Handlers { if (url.pathname === "/healthz" || url.pathname === "/__wrnexus/health") { return secure(Response.json({ status: "ok" })); } + if (webVitalsEnabled && url.pathname === webVitalsEndpoint) { + return secure(await webVitalsHandler(req)); + } + if (webVitalsEnabled && url.pathname === "/__wrnexus/vitals.js") { + return secure( + new Response( + webVitalsClient({ + endpoint: webVitalsEndpoint, + sampleRate: deps.observability?.sampleRate, + }), + { + headers: { + "content-type": "text/javascript; charset=utf-8", + "cache-control": url.searchParams.has("v") + ? "public, max-age=31536000, immutable" + : "no-cache", + }, + }, + ), + ); + } if (url.pathname === "/site.webmanifest" && pwaEnabled) { const pwa = pwaConfig; @@ -1455,6 +1493,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers { const scripts = collectScripts(body, deps.clientRuntimes, deps.navigation).map((script) => versionRenderScript(script, deps.assetVersion), ); + if (webVitalsEnabled) { + scripts.push(versionAssetUrl("/__wrnexus/vitals.js", deps.assetVersion)); + } if (pwaServiceWorkerEnabled) scripts.push(versionAssetUrl("/__wrnexus/pwa.js", deps.assetVersion)); if (deps.mobile?.enabled !== false && usesMobileRuntime(body)) @@ -1653,8 +1694,10 @@ const COMPRESS_MIN_BYTES = 1024; * `Cache-Control: no-transform`, so they are never buffered here. */ async function compressResponse(req: Request, res: Response): Promise { - const accept = req.headers.get("accept-encoding") ?? ""; - if (!accept.toLowerCase().includes("gzip")) return res; + const accept = (req.headers.get("accept-encoding") ?? "").toLowerCase(); + const acceptsBrotli = /(?:^|,)\s*br(?:\s*;|\s*,|$)/.test(accept); + const acceptsGzip = /(?:^|,)\s*gzip(?:\s*;|\s*,|$)/.test(accept); + if (!acceptsBrotli && !acceptsGzip) return res; if (res.headers.get("content-encoding")) return res; if (res.status === 204 || res.status === 304) return res; if (!COMPRESSIBLE_TYPE.test(res.headers.get("content-type") ?? "")) return res; @@ -1668,14 +1711,34 @@ async function compressResponse(req: Request, res: Response): Promise headers: res.headers, }); } - const gzipped = Bun.gzipSync(body); + + let encoded: Uint8Array; + let encoding: "br" | "gzip"; + if (acceptsBrotli) { + encoded = new Uint8Array( + brotliCompressSync(body, { + params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 4 }, + }), + ); + encoding = "br"; + } else { + encoded = Bun.gzipSync(body); + encoding = "gzip"; + } + const headers = new Headers(res.headers); - headers.set("content-encoding", "gzip"); - headers.set("content-length", String(gzipped.length)); + headers.set("content-encoding", encoding); + headers.set("content-length", String(encoded.length)); const vary = headers.get("Vary"); if (!vary) headers.set("Vary", "Accept-Encoding"); else if (!/\baccept-encoding\b/i.test(vary)) headers.set("Vary", `${vary}, Accept-Encoding`); - return new Response(gzipped, { status: res.status, statusText: res.statusText, headers }); + const responseBody = new ArrayBuffer(encoded.byteLength); + new Uint8Array(responseBody).set(encoded); + return new Response(responseBody, { + status: res.status, + statusText: res.statusText, + headers, + }); } /** Opening tag of a component mount: captures tag, attrs, name, self-close. */ @@ -1842,11 +1905,9 @@ export function normalizeComponentName(name: string): string { export function collectScripts( body: string, clientRuntimes: readonly ClientRuntimeDefinition[] = [], - navigation: { mode?: "client" | "document" } = {}, + navigation: { mode?: "auto" | "client" | "document" } = {}, ): RenderScript[] { - // Client navigation is optional. In document mode, links retain native browser - // behavior and each route receives a fresh server-rendered HTML document. - const scripts: RenderScript[] = navigation.mode === "document" ? [] : ["/__wrnexus/nav.js"]; + const scripts: RenderScript[] = []; if (/\bdata-scope=/.test(body) || /\bdata-wrnexus-csr=/.test(body)) { scripts.push("/__wrnexus/reactive.js"); } @@ -1877,6 +1938,15 @@ export function collectScripts( // `data-wrnexus-runtime="id"`; the corresponding package chunk is loaded // once, without requiring application-authored script tags or public copies. scripts.push(...runtimeScriptsForMarkup(body, clientRuntimes)); + + // `auto` is the performance-first default: a fully static page ships no + // framework JavaScript and its links use native document navigation. Routes + // that already need browser behavior also receive progressive navigation. + // `client` preserves the explicit always-on behavior; `document` disables it. + const mode = navigation.mode ?? "auto"; + if (mode === "client" || (mode === "auto" && scripts.length > 0)) { + scripts.unshift("/__wrnexus/nav.js"); + } return scripts; } diff --git a/packages/dev-server/test/plugin-assets.test.ts b/packages/dev-server/test/plugin-assets.test.ts index e1af6969..2155d26e 100644 --- a/packages/dev-server/test/plugin-assets.test.ts +++ b/packages/dev-server/test/plugin-assets.test.ts @@ -73,3 +73,20 @@ test("collectScripts omits client navigation in document mode", () => { expect(scripts).not.toContain("/__wrnexus/nav.js"); }); + +test("collectScripts ships zero framework JavaScript for static markup by default", () => { + expect(collectScripts('
About
')).toEqual([]); +}); + +test("collectScripts adds navigation in explicit client mode", () => { + expect(collectScripts("
Server rendered
", [], { mode: "client" })).toEqual([ + "/__wrnexus/nav.js", + ]); +}); + +test("collectScripts auto-adds navigation when a page already needs a browser runtime", () => { + expect(collectScripts('')).toEqual([ + "/__wrnexus/nav.js", + "/__wrnexus/reactive.js", + ]); +}); diff --git a/packages/dev-toolbar/package.json b/packages/dev-toolbar/package.json index 040e3114..4857a3c5 100644 --- a/packages/dev-toolbar/package.json +++ b/packages/dev-toolbar/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-toolbar", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/dev-toolbar/src/client/runtime.ts b/packages/dev-toolbar/src/client/runtime.ts index e6d6389f..1661c061 100644 --- a/packages/dev-toolbar/src/client/runtime.ts +++ b/packages/dev-toolbar/src/client/runtime.ts @@ -1,7 +1,7 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => { if (window.__wrnexusDevToolbar) return; const KEY = "__wrnexus_dev_toolbar_settings__"; - const state = { open:false, issues:[], report:null, search:"", severity:"all", category:"all", platform:null, panels:[], config:{ position:"bottom-center", scanOnNavigation:true, scanOnHmr:true } }; + const state = { open:false, issues:[], report:null, search:"", severity:"all", category:"all", platform:null, panels:[], runtimeMetrics:{ hydrationCount:0, hydrationMs:0, longTasks:[] }, config:{ position:"bottom-center", scanOnNavigation:true, scanOnHmr:true } }; try { state.config = Object.assign(state.config, JSON.parse(localStorage.getItem(KEY) || "{}")); } catch {} const host = document.createElement("wrnexus-dev-toolbar"); host.setAttribute("data-wrnexus-dev-toolbar", ""); @@ -36,16 +36,21 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => { const count=document.querySelectorAll("*").length;if(count>1500)found.push(issue("html/dom-size","html",count>3000?"error":"warning","DOM is large","The page contains "+count+" elements.",null,"Reduce wrappers and render hidden content on demand.")); q("button button,button a,a a,a button").forEach(el=>found.push(issue("html/nested-interactive","html","error","Interactive elements are nested","Nested links or buttons create invalid interaction behavior.",el,"Use one interactive element."))); q("a").forEach(el=>{const href=el.getAttribute("href");if(!href||href==="#")found.push(issue("links/empty","links","warning","Link has no useful destination","The href is missing or only #.",el,"Use a real URL or a button."));if(el.target==="_blank"&&!/\b(noopener|noreferrer)\b/.test(el.rel))found.push(issue("links/blank-rel","security","warning","New-tab link lacks rel protection","The opened page may access window.opener.",el,"Add rel=\"noopener noreferrer\"."));}); + q("form").forEach(el=>{const method=(el.getAttribute("method")||"get").toLowerCase();const action=el.getAttribute("action")||"";if(location.protocol==="https:"&&action.startsWith("http://"))found.push(issue("security/insecure-form-action","security","error","Form submits over HTTP",action,el,"Use an HTTPS or same-origin form action."));if(method!=="get"&&!el.hasAttribute("data-csrf")&&!el.querySelector('input[name="wire-csrf"],input[name="_csrf"]'))found.push(issue("security/missing-csrf","security","error","State-changing form has no CSRF marker","Cookie-authenticated form submissions require CSRF protection.",el,"Add data-csrf or a framework-generated CSRF field."));}); + q("script:not([src]):not([type='application/json']):not([nonce])").forEach(el=>found.push(issue("security/inline-script","security","warning","Inline script has no CSP nonce","A strict Content Security Policy will block this script.",el,"Move code to a client module or attach the request nonce."))); + q("iframe:not([sandbox])").forEach(el=>found.push(issue("security/iframe-sandbox","security","warning","Iframe is not sandboxed","Untrusted iframe content has broad browser capabilities.",el,"Add the narrowest sandbox and permissions policy."))); + q("[src],[href],[action]").forEach(el=>{const raw=el.getAttribute("src")||el.getAttribute("href")||el.getAttribute("action")||"";if(location.protocol==="https:"&&raw.startsWith("http://"))found.push(issue("security/mixed-content","security","error","Mixed-content resource",raw,el,"Use HTTPS or a same-origin resource."));}); + try{for(let i=0;iinnerWidth+2)found.push(issue("responsive/document-overflow","responsive","error","Page has horizontal overflow","Document width exceeds the viewport.",null,"Inspect fixed widths, long text and overflowing media.")); q("body *").filter(visible).slice(0,2500).forEach(el=>{const r=el.getBoundingClientRect();if((r.right>innerWidth+8||r.left<-8)&&found.filter(x=>x.ruleId==="responsive/element-overflow").length<20)found.push(issue("responsive/element-overflow","responsive","warning","Element extends outside the viewport","Element bounds exceed the current viewport.",el,"Use fluid sizing, wrapping, max-width or an intentional scroll container."));}); - const resources=performance.getEntriesByType("resource");const total=resources.reduce((s,e)=>s+(e.transferSize||0),0);if(resources.length>150)found.push(issue("performance/resource-count","performance","warning","Page loads many resources","Found "+resources.length+" resource requests.",null,"Remove duplicates and defer non-critical resources."));if(total>5000000)found.push(issue("performance/transfer-size","performance",total>10000000?"error":"warning","Page transfer size is large","Observed transfer size is about "+(total/1000000).toFixed(2)+" MB.",null,"Compress and optimize page resources.")); + const resources=performance.getEntriesByType("resource");const total=resources.reduce((s,e)=>s+(e.transferSize||0),0);const jsBytes=resources.filter(e=>/(?:\.m?js)(?:\?|$)/i.test(e.name)).reduce((sum,e)=>sum+(e.transferSize||0),0);if(resources.length>150)found.push(issue("performance/resource-count","performance","warning","Page loads many resources","Found "+resources.length+" resource requests.",null,"Remove duplicates and defer non-critical resources."));if(total>5000000)found.push(issue("performance/transfer-size","performance",total>10000000?"error":"warning","Page transfer size is large","Observed transfer size is about "+(total/1000000).toFixed(2)+" MB.",null,"Compress and optimize page resources."));if(jsBytes>150000)found.push(issue("performance/javascript-budget","javascript",jsBytes>300000?"error":"warning","JavaScript budget exceeded","JavaScript transfer is about "+(jsBytes/1000).toFixed(1)+" KB.",null,"Split routes and defer optional hydration."));const hydrationRoots=q("[data-wrn-client-module],[data-wrn-hydrate]");if(hydrationRoots.length>50)found.push(issue("performance/hydration-count","runtime","warning","Many components hydrate",hydrationRoots.length+" hydration boundaries were found.",null,"Use visible, idle or interaction hydration."));if(state.runtimeMetrics.longTasks.length)found.push(issue("performance/long-tasks","javascript","warning","Long main-thread tasks detected",state.runtimeMetrics.longTasks.length+" task(s) exceeded 50 ms.",null,"Split expensive work and reduce hydration.","high",{longestMs:Math.max(...state.runtimeMetrics.longTasks)})); q("[data-wrn-client-module]").forEach(el=>found.push(issue("runtime/client-module","runtime","info","Client function module",el.getAttribute("data-wrn-client-module")||"Unknown module",el,"Loaded according to the component hydration strategy.","high",{hydration:el.getAttribute("data-wrn-hydrate"),runtime:el.getAttribute("data-wrn-runtime")}))); const storeContainer=window.__wrnexusStoreContainer; if(storeContainer&&typeof storeContainer.inspect==="function"){ for(const store of storeContainer.inspect())found.push(issue("stores/instance","stores","info",store.kind+" store: "+store.name,JSON.stringify({state:store.state,computed:store.computed}),null,"Use store actions for mutations. Sensitive server state is never hydrated.","high",store)); } const unique=new Map();[...state.issues.filter(x=>["runtime","stores","network","server","compiler","routing"].includes(x.category)),...found].forEach(x=>unique.set(x.fingerprint,x));state.issues=[...unique.values()]; - state.report={url:location.href,pathname:location.pathname,title:document.title,status:200,generatedAt:Date.now(),issues:state.issues,metrics:{domNodes:count,cssResources:q('link[rel="stylesheet"]').length,scriptResources:q("script[src]").length,imageResources:q("img").length,totalTransferBytes:total,pageLoadMs:performance.getEntriesByType("navigation")[0]?.duration}}; render(); return state.report; + state.report={url:location.href,pathname:location.pathname,title:document.title,status:200,generatedAt:Date.now(),issues:state.issues,metrics:{domNodes:count,cssResources:q('link[rel="stylesheet"]').length,scriptResources:q("script[src]").length,imageResources:q("img").length,totalTransferBytes:total,javascriptBytes:jsBytes,hydratedComponents:state.runtimeMetrics.hydrationCount,hydrationMs:state.runtimeMetrics.hydrationMs,longTasks:state.runtimeMetrics.longTasks.length,pageLoadMs:performance.getEntriesByType("navigation")[0]?.duration}}; render(); return state.report; }; let highlightEl=null; const clearHighlight=()=>{highlightEl?.remove();highlightEl=null}; const highlight=sel=>{clearHighlight();let target;try{target=document.querySelector(sel)}catch{}if(!target)return;const r=target.getBoundingClientRect();highlightEl=document.createElement("div");highlightEl.className="wrn-highlight";Object.assign(highlightEl.style,{left:r.left+"px",top:r.top+"px",width:r.width+"px",height:r.height+"px"});root.appendChild(highlightEl);target.scrollIntoView({block:"center",behavior:"smooth"});setTimeout(clearHighlight,3000)}; const render=()=>{const filtered=state.issues.filter(x=>{const severityMatches=state.severity==="all"||x.severity===state.severity;const categoryMatches=state.category==="all"||x.category===state.category;const textMatches=!state.search||[x.title,x.message,x.ruleId,x.category,x.severity,x.source?.file].join(" ").toLowerCase().includes(state.search);return severityMatches&&categoryMatches&&textMatches;});root.querySelector("[data-errors]").textContent=state.issues.filter(x=>x.severity==="error").length;root.querySelector("[data-warnings]").textContent=state.issues.filter(x=>x.severity==="warning").length;root.querySelector("[data-suggestions]").textContent=state.issues.filter(x=>x.severity==="suggestion").length;root.querySelectorAll("[data-filter]").forEach(button=>{const active=button.dataset.filter===state.severity;button.classList.toggle("active",active);button.setAttribute("aria-pressed",String(active));});root.querySelectorAll("[data-category]").forEach(button=>{const active=button.dataset.category===state.category;button.classList.toggle("active",active);button.setAttribute("aria-pressed",String(active));});meta.textContent=location.pathname+" · "+state.issues.length+" findings"+(state.category!=="all"?" · "+state.category:"")+(state.severity!=="all"?" · "+state.severity:"");if(!filtered.length){list.innerHTML='
No issues match the current filters.
';return;}list.innerHTML=filtered.map(x=>'
'+esc(x.title)+'
'+esc(x.message)+'
'+esc(x.severity)+' · '+esc(x.category)+' · '+esc(x.ruleId)+(x.source?.file?' · '+esc(x.source.file):'')+'
'+(x.target?.selector?'':'')+'
').join("");}; @@ -57,6 +62,8 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => { addEventListener("wrnexus:navigated",()=>{if(state.config.scanOnNavigation)setTimeout(scan,50)});addEventListener("wrnexus:hmr",()=>{if(state.config.scanOnHmr)setTimeout(scan,100)});addEventListener("wrnexus:runtime-error",event=>addRuntime("error","WRNexus runtime error",event.detail?.message||"Runtime failure",event.detail||{})); addEventListener("wrnexus:diagnostic",event=>{const detail=event.detail||{};const code=detail.code||"WRN-RUNTIME";const category=String(code).includes("HYDRATE")?"runtime":String(code).includes("ROUTE")?"routing":"compiler";const title=String(code).includes("HYDRATE")?"Hydration diagnostic":"WRNexus diagnostic";const x=issue(String(code),category,"error",title,detail.message||"Framework diagnostic",null,"Open the source location and resolve the reported framework contract.","high",detail);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();}); addEventListener("wrnexus:store-mutation",event=>{const mutation=event.detail||{};const x=issue("stores/mutation","stores","info","Store action: "+(mutation.store||"unknown")+"."+(mutation.action||"direct"),"Changed fields: "+((mutation.changed||[]).join(", ")||"none"),null,"Use the Stores panel to inspect safe client state.","high",mutation);state.issues=[x,...state.issues.filter(i=>i.fingerprint!==x.fingerprint)];render();}); + addEventListener("wrnexus:hydrated",event=>{state.runtimeMetrics.hydrationCount+=1;state.runtimeMetrics.hydrationMs+=Number(event.detail?.durationMs||0);}); + try{new PerformanceObserver(list=>{for(const entry of list.getEntries()){if(entry.duration>50)state.runtimeMetrics.longTasks.push(entry.duration);}state.runtimeMetrics.longTasks=state.runtimeMetrics.longTasks.slice(-100);}).observe({type:"longtask",buffered:true});}catch{} fetch("/__wrnexus/dev-toolbar/platform").then(r=>r.ok?r.json():null).then(data=>{if(!data)return;state.platform=data.platform;state.panels=data.panels||[];render();}).catch(()=>{}); window.__wrnexusDevToolbar={open(){state.open=true;panel.classList.add("open")},close(){state.open=false;panel.classList.remove("open")},toggle(){state.open=!state.open;panel.classList.toggle("open",state.open)},scan,clear(){state.issues=[];render()},report(){return state.report},highlight,configure(config){state.config=Object.assign(state.config,config||{});shell.dataset.position=state.config.position;try{localStorage.setItem(KEY,JSON.stringify(state.config))}catch{}}}; const observer=new MutationObserver(()=>{clearTimeout(observer.timer);observer.timer=setTimeout(()=>{if(!state.open)return;scan()},400)});observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:["class","style","src","href","alt","aria-label"]}); diff --git a/packages/dev-toolbar/src/rules/performance.ts b/packages/dev-toolbar/src/rules/performance.ts index 57dadeea..b465374d 100644 --- a/packages/dev-toolbar/src/rules/performance.ts +++ b/packages/dev-toolbar/src/rules/performance.ts @@ -6,13 +6,13 @@ export const performanceRules: DevToolbarRule[] = [ id: "performance/resources", category: "performance", defaultSeverity: "warning", - description: "Checks resource count and transfer sizes.", - run: ({ performanceEntries }) => { + description: "Checks resource, DOM, hydration, and main-thread budgets.", + run: ({ document, root, performanceEntries }) => { const resources = performanceEntries.filter( (entry): entry is PerformanceResourceTiming => entry.entryType === "resource", ); const issues = []; - if (resources.length > 150) + if (resources.length > 150) { issues.push( createIssue({ ruleId: "performance/resource-count", @@ -24,8 +24,9 @@ export const performanceRules: DevToolbarRule[] = [ "Remove duplicates, combine tiny assets where useful, and load non-critical resources later.", }), ); + } const total = resources.reduce((sum, entry) => sum + (entry.transferSize || 0), 0); - if (total > 5_000_000) + if (total > 5_000_000) { issues.push( createIssue({ ruleId: "performance/transfer-size", @@ -37,7 +38,26 @@ export const performanceRules: DevToolbarRule[] = [ "Compress images, scripts, styles and fonts; review third-party resources.", }), ); - for (const entry of resources.filter((item) => item.duration > 2000).slice(0, 20)) + } + + const javascriptBytes = resources + .filter((entry) => /(?:\.m?js)(?:\?|$)/i.test(entry.name)) + .reduce((sum, entry) => sum + (entry.transferSize || 0), 0); + if (javascriptBytes > 150_000) { + issues.push( + createIssue({ + ruleId: "performance/javascript-budget", + category: "javascript", + severity: javascriptBytes > 300_000 ? "error" : "warning", + title: "JavaScript budget exceeded", + message: `JavaScript transfer is approximately ${(javascriptBytes / 1_000).toFixed(1)} KB.`, + recommendation: + "Split routes, remove unused client code, and defer optional hydration.", + }), + ); + } + + for (const entry of resources.filter((item) => item.duration > 2_000).slice(0, 20)) { issues.push( createIssue({ ruleId: "performance/slow-resource", @@ -49,6 +69,90 @@ export const performanceRules: DevToolbarRule[] = [ metadata: { url: entry.name, duration: entry.duration }, }), ); + } + + const domNodes = root.querySelectorAll("*").length; + if (domNodes > 1_500) { + issues.push( + createIssue({ + ruleId: "performance/dom-size", + category: "performance", + severity: domNodes > 3_000 ? "error" : "warning", + title: "DOM is large", + message: `The page contains ${domNodes} elements.`, + recommendation: + "Virtualize long lists and avoid rendering hidden or duplicate structures.", + }), + ); + } + + const hydrationRoots = root.querySelectorAll("[data-wrn-client-module], [data-wrn-hydrate]"); + if (hydrationRoots.length > 50) { + issues.push( + createIssue({ + ruleId: "performance/hydration-count", + category: "runtime", + severity: "warning", + title: "Many components hydrate", + message: `${hydrationRoots.length} hydration boundaries were found.`, + recommendation: + "Use visible, idle, or interaction hydration and keep static components server-only.", + }), + ); + } + + for (const image of root.querySelectorAll("img")) { + if (!image.complete || !image.naturalWidth) continue; + const renderedWidth = Math.max(1, image.getBoundingClientRect().width); + if (image.naturalWidth > renderedWidth * 2.5) { + issues.push( + createIssue({ + ruleId: "performance/oversized-image", + category: "images", + severity: "warning", + title: "Image is larger than rendered size", + message: `${image.naturalWidth}px image is rendered at about ${Math.round(renderedWidth)}px.`, + element: image, + recommendation: "Generate responsive srcset candidates and accurate sizes.", + }), + ); + } + } + + const blocking = document.querySelectorAll( + 'head script:not([async]):not([defer]):not([type="module"]), head link[rel="stylesheet"]:not([media])', + ); + if (blocking.length > 4) { + issues.push( + createIssue({ + ruleId: "performance/render-blocking", + category: "performance", + severity: "warning", + title: "Multiple render-blocking resources", + message: `${blocking.length} potentially render-blocking resources were found.`, + recommendation: "Inline only critical CSS and defer non-critical scripts and styles.", + }), + ); + } + + const longTasks = performanceEntries.filter( + (entry) => entry.entryType === "longtask" && entry.duration > 50, + ); + if (longTasks.length) { + issues.push( + createIssue({ + ruleId: "performance/long-tasks", + category: "javascript", + severity: "warning", + title: "Long main-thread tasks detected", + message: `${longTasks.length} task(s) exceeded 50 ms.`, + recommendation: + "Split expensive work, reduce hydration, and move non-UI work off the main thread.", + metadata: { longestMs: Math.max(...longTasks.map((entry) => entry.duration)) }, + }), + ); + } + return issues; }, }, diff --git a/packages/dev-toolbar/src/rules/security.ts b/packages/dev-toolbar/src/rules/security.ts index 25d167c1..f8174854 100644 --- a/packages/dev-toolbar/src/rules/security.ts +++ b/packages/dev-toolbar/src/rules/security.ts @@ -1,16 +1,18 @@ import type { DevToolbarRule } from "./types.ts"; import { createIssue } from "./helpers.ts"; +const SECRET_KEY = /pass(word)?|token|secret|api[-_]?key|authorization|session/i; + export const securityRules: DevToolbarRule[] = [ { id: "security/page", category: "security", defaultSeverity: "warning", description: "Checks development-visible security mistakes.", - run: ({ url, root }) => { + run: ({ document, window, url, root }) => { const issues = []; - for (const [key] of url.searchParams) - if (/pass(word)?|token|secret|api[-_]?key/i.test(key)) + for (const [key] of url.searchParams) { + if (SECRET_KEY.test(key)) { issues.push( createIssue({ ruleId: "security/secret-query", @@ -22,7 +24,10 @@ export const securityRules: DevToolbarRule[] = [ "Send secrets in a secure request body or authorization header, not a URL.", }), ); - for (const form of root.querySelectorAll('form[action^="http://"]')) + } + } + + for (const form of root.querySelectorAll('form[action^="http://"]')) { issues.push( createIssue({ ruleId: "security/insecure-form", @@ -34,6 +39,133 @@ export const securityRules: DevToolbarRule[] = [ recommendation: "Submit to an HTTPS endpoint.", }), ); + } + + for (const form of root.querySelectorAll("form")) { + const method = (form.method || "get").toUpperCase(); + if ( + !["GET", "HEAD"].includes(method) && + !form.querySelector('input[name="wire-csrf"], input[name="_csrf"]') + ) { + issues.push( + createIssue({ + ruleId: "security/missing-csrf", + category: "security", + severity: "error", + title: "State-changing form has no CSRF token", + message: `${method} form does not contain a recognized CSRF field.`, + element: form, + recommendation: "Enable WRNexus CSRF middleware and use the generated form token.", + }), + ); + } + } + + for (const script of root.querySelectorAll("script:not([src])")) { + if (script.type === "application/json" || script.hasAttribute("nonce")) continue; + issues.push( + createIssue({ + ruleId: "security/inline-script", + category: "security", + severity: "warning", + title: "Inline script has no CSP nonce", + message: "A strict Content Security Policy will block this inline script.", + element: script, + recommendation: "Move code into a client module or attach the request CSP nonce.", + }), + ); + } + + for (const element of root.querySelectorAll("[style]")) { + issues.push( + createIssue({ + ruleId: "security/inline-style", + category: "security", + severity: "suggestion", + title: "Inline style weakens strict CSP", + message: "The element uses a style attribute.", + element, + recommendation: + "Prefer extracted CSS classes or a nonce/hash-compatible style strategy.", + confidence: "medium", + }), + ); + } + + for (const element of root.querySelectorAll("[src], [href], [action]")) { + const raw = + element.getAttribute("src") ?? + element.getAttribute("href") ?? + element.getAttribute("action"); + if (window.location.protocol === "https:" && raw?.startsWith("http://")) { + issues.push( + createIssue({ + ruleId: "security/mixed-content", + category: "security", + severity: "error", + title: "Mixed-content resource", + message: `${raw} is loaded over insecure HTTP.`, + element, + recommendation: "Use HTTPS or serve the resource from the same secure origin.", + }), + ); + } + } + + for (const frame of root.querySelectorAll("iframe:not([sandbox])")) { + issues.push( + createIssue({ + ruleId: "security/iframe-sandbox", + category: "security", + severity: "warning", + title: "Iframe is not sandboxed", + message: "Third-party or untrusted iframe content has broad browser capabilities.", + element: frame, + recommendation: "Add the narrowest possible sandbox and permissions policy.", + }), + ); + } + + try { + for (let index = 0; index < window.localStorage.length; index += 1) { + const key = window.localStorage.key(index) ?? ""; + if (SECRET_KEY.test(key)) { + issues.push( + createIssue({ + ruleId: "security/sensitive-local-storage", + category: "security", + severity: "error", + title: "Sensitive value may be stored in localStorage", + message: `Storage key “${key}” looks authentication- or secret-related.`, + recommendation: "Keep sessions and credentials in Secure, HttpOnly cookies.", + }), + ); + } + } + } catch { + // Storage may be unavailable in restricted browser contexts. + } + + const hydration = document.querySelectorAll( + 'script[type="application/json"][data-wrnexus-state], script[data-wrnexus-store-state]', + ); + for (const script of hydration) { + if (SECRET_KEY.test(script.textContent ?? "")) { + issues.push( + createIssue({ + ruleId: "security/hydration-secret", + category: "security", + severity: "error", + title: "Hydration payload may contain a secret", + message: "Sensitive-looking field names were found in serialized client state.", + element: script, + recommendation: "Move sensitive state to server-only state and regenerate the page.", + confidence: "medium", + }), + ); + } + } + return issues; }, }, diff --git a/packages/dev-toolbar/src/types.ts b/packages/dev-toolbar/src/types.ts index b50bedfb..d1314617 100644 --- a/packages/dev-toolbar/src/types.ts +++ b/packages/dev-toolbar/src/types.ts @@ -79,6 +79,13 @@ export interface DevToolbarMetrics { longTasks?: number; layoutShifts?: number; pageLoadMs?: number; + hydrationMs?: number; + hydratedComponents?: number; + javascriptBytes?: number; + cssBytes?: number; + lcpMs?: number; + cls?: number; + inpMs?: number; } export interface DevToolbarPageReport { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index e8aa5f14..4c643e75 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index a829d012..14996448 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 555ecf51..3c79f31a 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/image/README.md b/packages/image/README.md new file mode 100644 index 00000000..086ace57 --- /dev/null +++ b/packages/image/README.md @@ -0,0 +1,16 @@ +# @wrnexus/image + +Responsive image attribute generation with secure remote-host policies and audits for dimensions, LCP loading, source oversizing, transfer size, and modern formats. + +```ts +import { createResponsiveImage } from "@wrnexus/image"; +const attrs = createResponsiveImage({ + src: "/hero.jpg", + alt: "Hero", + width: 1600, + height: 900, + widths: [480, 960, 1600], + sizes: "100vw", + format: "avif", +}); +``` diff --git a/packages/image/package.json b/packages/image/package.json new file mode 100644 index 00000000..24f49c54 --- /dev/null +++ b/packages/image/package.json @@ -0,0 +1,13 @@ +{ + "name": "@wrnexus/image", + "version": "0.7.0", + "type": "module", + "description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@wrnexus/security": "workspace:*" + } +} diff --git a/packages/image/src/index.ts b/packages/image/src/index.ts new file mode 100644 index 00000000..c6bb4e9e --- /dev/null +++ b/packages/image/src/index.ts @@ -0,0 +1,188 @@ +import { validateUrl } from "@wrnexus/security"; + +export type ImageFormat = "avif" | "webp" | "jpeg" | "png" | "original"; + +export interface ImageLoaderInput { + src: string; + width: number; + quality?: number; + format?: ImageFormat; +} + +export type ImageLoader = (input: ImageLoaderInput) => string; + +export interface ImagePolicy { + remoteHosts?: string[]; + allowedProtocols?: string[]; + maxWidth?: number; + maxQuality?: number; +} + +export interface ResponsiveImageOptions extends ImagePolicy { + src: string; + alt: string; + width: number; + height: number; + widths?: number[]; + sizes?: string; + quality?: number; + format?: ImageFormat; + loading?: "eager" | "lazy"; + fetchPriority?: "high" | "low" | "auto"; + decoding?: "async" | "sync" | "auto"; + loader?: ImageLoader; + class?: string; +} + +export interface ResponsiveImageAttributes { + src: string; + srcset?: string; + sizes?: string; + alt: string; + width: string; + height: string; + loading: "eager" | "lazy"; + decoding: "async" | "sync" | "auto"; + fetchpriority?: "high" | "low" | "auto"; + class?: string; +} + +export interface ImageAuditInput { + src: string; + width?: number; + height?: number; + renderedWidth?: number; + bytes?: number; + loading?: string; + fetchPriority?: string; + isLcp?: boolean; +} + +export interface ImageAuditIssue { + code: string; + severity: "error" | "warning" | "info"; + message: string; +} + +export const defaultImageLoader: ImageLoader = ({ src, width, quality, format }) => { + const separator = src.includes("?") ? "&" : "?"; + const params = new URLSearchParams({ w: String(width) }); + if (quality !== undefined) params.set("q", String(quality)); + if (format && format !== "original") params.set("format", format); + return `${src}${separator}${params.toString()}`; +}; + +function validateSource(src: string, policy: ImagePolicy): void { + if (/^(?:https?:)?\/\//i.test(src)) { + validateUrl(src.startsWith("//") ? `https:${src}` : src, { + allowRelative: false, + allowedProtocols: policy.allowedProtocols ?? ["https:"], + allowedHosts: policy.remoteHosts, + allowCredentials: false, + }); + } else { + validateUrl(src, { + base: "https://wrnexus.invalid", + allowRelative: true, + allowedProtocols: ["https:"], + allowCredentials: false, + }); + } +} + +export function createResponsiveImage(options: ResponsiveImageOptions): ResponsiveImageAttributes { + validateSource(options.src, options); + if (!Number.isFinite(options.width) || options.width <= 0) { + throw new RangeError("Image width must be a positive number."); + } + if (!Number.isFinite(options.height) || options.height <= 0) { + throw new RangeError("Image height must be a positive number."); + } + const maxWidth = options.maxWidth ?? 8_192; + const maxQuality = options.maxQuality ?? 100; + const quality = Math.min(maxQuality, Math.max(1, Math.round(options.quality ?? 80))); + const widths = [...new Set(options.widths ?? [options.width])] + .map((width) => Math.round(width)) + .filter((width) => width > 0 && width <= maxWidth) + .sort((a, b) => a - b); + if (!widths.includes(Math.round(options.width)) && options.width <= maxWidth) { + widths.push(Math.round(options.width)); + widths.sort((a, b) => a - b); + } + const loader = options.loader ?? defaultImageLoader; + const src = loader({ + src: options.src, + width: Math.min(Math.round(options.width), maxWidth), + quality, + format: options.format, + }); + const srcset = + widths.length > 1 + ? widths + .map( + (width) => + `${loader({ src: options.src, width, quality, format: options.format })} ${width}w`, + ) + .join(", ") + : undefined; + + return { + src, + ...(srcset ? { srcset } : {}), + ...(options.sizes ? { sizes: options.sizes } : {}), + alt: options.alt, + width: String(Math.round(options.width)), + height: String(Math.round(options.height)), + loading: options.loading ?? (options.fetchPriority === "high" ? "eager" : "lazy"), + decoding: options.decoding ?? "async", + ...(options.fetchPriority ? { fetchpriority: options.fetchPriority } : {}), + ...(options.class ? { class: options.class } : {}), + }; +} + +export function auditImage(input: ImageAuditInput): ImageAuditIssue[] { + const issues: ImageAuditIssue[] = []; + if (!input.width || !input.height) { + issues.push({ + code: "WRN-IMAGE-DIMENSIONS", + severity: "error", + message: "Images must declare width and height to prevent layout shifts.", + }); + } + if (input.isLcp && input.loading === "lazy") { + issues.push({ + code: "WRN-IMAGE-LCP-LAZY", + severity: "error", + message: "The LCP image must not be lazy-loaded.", + }); + } + if (input.isLcp && input.fetchPriority !== "high") { + issues.push({ + code: "WRN-IMAGE-LCP-PRIORITY", + severity: "warning", + message: "Consider fetchpriority=high for the LCP image.", + }); + } + if (input.width && input.renderedWidth && input.width > input.renderedWidth * 2.5) { + issues.push({ + code: "WRN-IMAGE-OVERSIZED", + severity: "warning", + message: "The source image is substantially wider than its rendered size.", + }); + } + if ((input.bytes ?? 0) > 1_000_000) { + issues.push({ + code: "WRN-IMAGE-BYTES", + severity: (input.bytes ?? 0) > 3_000_000 ? "error" : "warning", + message: `Image transfer size is ${Math.round((input.bytes ?? 0) / 1024)} KiB.`, + }); + } + if (/\.(?:png|jpe?g)(?:\?|$)/i.test(input.src)) { + issues.push({ + code: "WRN-IMAGE-MODERN-FORMAT", + severity: "info", + message: "Consider serving AVIF or WebP with a compatible fallback.", + }); + } + return issues; +} diff --git a/packages/image/test/image.test.ts b/packages/image/test/image.test.ts new file mode 100644 index 00000000..0186d1fb --- /dev/null +++ b/packages/image/test/image.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test"; +import { auditImage, createResponsiveImage } from "../src/index.ts"; + +describe("@wrnexus/image", () => { + test("builds deterministic responsive image attributes", () => { + const image = createResponsiveImage({ + src: "/hero.jpg", + alt: "Hero", + width: 1200, + height: 600, + widths: [400, 800, 1200, 800], + sizes: "100vw", + format: "webp", + fetchPriority: "high", + }); + expect(image.srcset).toContain("400w"); + expect(image.srcset).toContain("1200w"); + expect(image.loading).toBe("eager"); + expect(image.width).toBe("1200"); + }); + + test("reports layout shift and LCP mistakes", () => { + const issues = auditImage({ src: "hero.jpg", isLcp: true, loading: "lazy" }); + expect(issues.map((issue) => issue.code)).toContain("WRN-IMAGE-DIMENSIONS"); + expect(issues.map((issue) => issue.code)).toContain("WRN-IMAGE-LCP-LAZY"); + }); + test("rejects insecure remote images unless explicitly allowed", () => { + expect(() => + createResponsiveImage({ + src: "http://images.example.com/hero.jpg", + alt: "Hero", + width: 800, + height: 400, + remoteHosts: ["images.example.com"], + }), + ).toThrow(); + }); +}); diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 1e9aff5a..ef1cffe8 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index ad18ce28..d623b136 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 3b489518..b2272b3f 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 8455c1ba..d5a0438a 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/observability/README.md b/packages/observability/README.md new file mode 100644 index 00000000..f4938ec9 --- /dev/null +++ b/packages/observability/README.md @@ -0,0 +1,9 @@ +# @wrnexus/observability + +Privacy-conscious counters, gauges, histograms, HTTP middleware, Web Vitals ingestion, browser collection, and exporter adapters. Request bodies and user identifiers are not collected by default. + +```ts +export default { + observability: { enabled: true, serverTiming: true, sampleRate: 0.1, webVitals: true }, +}; +``` diff --git a/packages/observability/package.json b/packages/observability/package.json new file mode 100644 index 00000000..95325909 --- /dev/null +++ b/packages/observability/package.json @@ -0,0 +1,14 @@ +{ + "name": "@wrnexus/observability", + "version": "0.7.0", + "type": "module", + "description": "Metrics, Web Vitals collection, request instrumentation, and exporter adapters for WRNexusJS.", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./client": "./src/client.ts" + }, + "dependencies": { + "@wrnexus/core": "workspace:*" + } +} diff --git a/packages/observability/src/client.ts b/packages/observability/src/client.ts new file mode 100644 index 00000000..f142c658 --- /dev/null +++ b/packages/observability/src/client.ts @@ -0,0 +1,29 @@ +export interface WebVitalsClientOptions { + endpoint?: string; + sampleRate?: number; +} + +export function webVitalsClient(options: WebVitalsClientOptions = {}): string { + const endpoint = JSON.stringify(options.endpoint ?? "/__wrnexus/metrics/vitals"); + const sampleRate = Math.max(0, Math.min(1, options.sampleRate ?? 0.1)); + return `(function(){ + if (!window.PerformanceObserver || Math.random() > ${sampleRate}) return; + var endpoint = ${endpoint}; + var cls = 0; + function rating(name, value) { + if (name === "LCP") return value <= 2500 ? "good" : value <= 4000 ? "needs-improvement" : "poor"; + if (name === "INP") return value <= 200 ? "good" : value <= 500 ? "needs-improvement" : "poor"; + if (name === "CLS") return value <= 0.1 ? "good" : value <= 0.25 ? "needs-improvement" : "poor"; + return "unknown"; + } + function send(name, value) { + var body = JSON.stringify({ name: name, value: value, rating: rating(name, value), route: location.pathname, navigationType: performance.getEntriesByType("navigation")[0]?.type }); + if (navigator.sendBeacon) navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" })); + else fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true }).catch(function(){}); + } + try { new PerformanceObserver(function(list){ var entries=list.getEntries(); var last=entries[entries.length-1]; if(last) send("LCP", last.startTime); }).observe({type:"largest-contentful-paint",buffered:true}); } catch(_) {} + try { new PerformanceObserver(function(list){ list.getEntries().forEach(function(e){ if(!e.hadRecentInput) cls += e.value; }); }).observe({type:"layout-shift",buffered:true}); } catch(_) {} + try { new PerformanceObserver(function(list){ var max=0; list.getEntries().forEach(function(e){ max=Math.max(max,e.duration||0); }); if(max) send("INP",max); }).observe({type:"event",durationThreshold:40,buffered:true}); } catch(_) {} + addEventListener("visibilitychange", function(){ if(document.visibilityState === "hidden" && cls) send("CLS", cls); }, { once: true }); +})();`; +} diff --git a/packages/observability/src/index.ts b/packages/observability/src/index.ts new file mode 100644 index 00000000..1ee2f5f8 --- /dev/null +++ b/packages/observability/src/index.ts @@ -0,0 +1,16 @@ +export { MetricsRegistry } from "./metrics.ts"; +export type { MetricLabels, MetricPoint } from "./metrics.ts"; +export { + createHttpMetricExporter, + createWebVitalsHandler, + defaultMetrics, + metricsMiddleware, +} from "./server.ts"; +export type { + MetricExporter, + MetricsMiddlewareOptions, + WebVitalRecord, + WebVitalsHandlerOptions, +} from "./server.ts"; +export { webVitalsClient } from "./client.ts"; +export type { WebVitalsClientOptions } from "./client.ts"; diff --git a/packages/observability/src/metrics.ts b/packages/observability/src/metrics.ts new file mode 100644 index 00000000..bf3c3551 --- /dev/null +++ b/packages/observability/src/metrics.ts @@ -0,0 +1,81 @@ +export type MetricLabels = Record; + +export interface MetricPoint { + name: string; + type: "counter" | "gauge" | "histogram"; + value: number; + count?: number; + sum?: number; + min?: number; + max?: number; + labels: MetricLabels; + timestamp: number; +} + +function labelKey(labels: MetricLabels): string { + return Object.entries(labels) + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([key, value]) => `${key}=${String(value)}`) + .join(","); +} + +interface StoredMetric extends MetricPoint { + key: string; +} + +export class MetricsRegistry { + private points = new Map(); + constructor(private readonly clock: () => number = Date.now) {} + + increment(name: string, value = 1, labels: MetricLabels = {}): void { + const key = `counter:${name}:${labelKey(labels)}`; + const current = this.points.get(key); + this.points.set(key, { + key, + name, + type: "counter", + value: (current?.value ?? 0) + value, + labels: { ...labels }, + timestamp: this.clock(), + }); + } + + gauge(name: string, value: number, labels: MetricLabels = {}): void { + const key = `gauge:${name}:${labelKey(labels)}`; + this.points.set(key, { + key, + name, + type: "gauge", + value, + labels: { ...labels }, + timestamp: this.clock(), + }); + } + + observe(name: string, value: number, labels: MetricLabels = {}): void { + const key = `histogram:${name}:${labelKey(labels)}`; + const current = this.points.get(key); + const count = (current?.count ?? 0) + 1; + const sum = (current?.sum ?? 0) + value; + this.points.set(key, { + key, + name, + type: "histogram", + value: sum / count, + count, + sum, + min: current?.min === undefined ? value : Math.min(current.min, value), + max: current?.max === undefined ? value : Math.max(current.max, value), + labels: { ...labels }, + timestamp: this.clock(), + }); + } + + snapshot(): MetricPoint[] { + return [...this.points.values()].map(({ key: _key, ...point }) => ({ ...point })); + } + + reset(): void { + this.points.clear(); + } +} diff --git a/packages/observability/src/server.ts b/packages/observability/src/server.ts new file mode 100644 index 00000000..b30e4be8 --- /dev/null +++ b/packages/observability/src/server.ts @@ -0,0 +1,132 @@ +import type { Context, Middleware } from "@wrnexus/core"; +import { MetricsRegistry, type MetricPoint } from "./metrics.ts"; + +export interface MetricsMiddlewareOptions { + registry?: MetricsRegistry; + routeLabel?: (ctx: Context) => string; + includePath?: boolean; +} + +export function metricsMiddleware(options: MetricsMiddlewareOptions = {}): Middleware { + const registry = options.registry ?? defaultMetrics; + let active = 0; + return async (ctx, next) => { + const started = performance.now(); + active++; + registry.gauge("http.server.active_requests", active); + try { + const response = await next(); + const route = + options.routeLabel?.(ctx) ?? (options.includePath ? ctx.url.pathname : "unknown"); + const labels = { method: ctx.req.method, status: response.status, route }; + registry.increment("http.server.requests", 1, labels); + registry.observe("http.server.duration_ms", performance.now() - started, labels); + return response; + } catch (error) { + registry.increment("http.server.errors", 1, { method: ctx.req.method }); + throw error; + } finally { + active--; + registry.gauge("http.server.active_requests", active); + } + }; +} + +export interface WebVitalRecord { + name: "LCP" | "INP" | "CLS" | "FCP" | "TTFB"; + value: number; + rating?: "good" | "needs-improvement" | "poor"; + route?: string; + navigationType?: string; +} + +export interface WebVitalsHandlerOptions { + registry?: MetricsRegistry; + onRecord?: (record: WebVitalRecord, request: Request) => void | Promise; + maxBodyBytes?: number; +} + +const VITAL_NAMES = new Set(["LCP", "INP", "CLS", "FCP", "TTFB"]); + +export function createWebVitalsHandler(options: WebVitalsHandlerOptions = {}) { + const registry = options.registry ?? defaultMetrics; + const maxBodyBytes = options.maxBodyBytes ?? 8 * 1024; + return async (request: Request): Promise => { + if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 }); + if (request.headers.get("sec-fetch-site") === "cross-site") { + return new Response("Forbidden", { status: 403 }); + } + const origin = request.headers.get("origin"); + if (origin) { + try { + if (new URL(origin).origin !== new URL(request.url).origin) { + return new Response("Forbidden", { status: 403 }); + } + } catch { + return new Response("Forbidden", { status: 403 }); + } + } + const length = Number(request.headers.get("content-length") ?? "0"); + if (length > maxBodyBytes) return new Response("Payload Too Large", { status: 413 }); + let body: unknown; + try { + const text = await request.text(); + if (new TextEncoder().encode(text).byteLength > maxBodyBytes) { + return new Response("Payload Too Large", { status: 413 }); + } + body = JSON.parse(text); + } catch { + return new Response("Invalid JSON", { status: 400 }); + } + if (!body || typeof body !== "object") return new Response("Invalid metric", { status: 400 }); + const raw = body as Record; + if ( + !VITAL_NAMES.has(String(raw.name)) || + typeof raw.value !== "number" || + !Number.isFinite(raw.value) + ) { + return new Response("Invalid metric", { status: 400 }); + } + const record: WebVitalRecord = { + name: raw.name as WebVitalRecord["name"], + value: raw.value, + ...(typeof raw.rating === "string" ? { rating: raw.rating as WebVitalRecord["rating"] } : {}), + ...(typeof raw.route === "string" ? { route: raw.route.slice(0, 256) } : {}), + ...(typeof raw.navigationType === "string" + ? { navigationType: raw.navigationType.slice(0, 64) } + : {}), + }; + registry.observe(`web.vitals.${record.name.toLowerCase()}`, record.value, { + rating: record.rating ?? "unknown", + route: record.route ?? "unknown", + }); + await options.onRecord?.(record, request); + return new Response(null, { status: 204, headers: { "cache-control": "no-store" } }); + }; +} + +export interface MetricExporter { + export(points: readonly MetricPoint[]): Promise | void; +} + +export function createHttpMetricExporter( + endpoint: string, + options: { headers?: HeadersInit; fetch?: typeof fetch } = {}, +): MetricExporter { + const send = options.fetch ?? fetch; + return { + async export(points) { + const response = await send(endpoint, { + method: "POST", + headers: { + "content-type": "application/json", + ...Object.fromEntries(new Headers(options.headers)), + }, + body: JSON.stringify({ resourceMetrics: points }), + }); + if (!response.ok) throw new Error(`Metric export failed with ${response.status}.`); + }, + }; +} + +export const defaultMetrics = new MetricsRegistry(); diff --git a/packages/observability/test/observability.test.ts b/packages/observability/test/observability.test.ts new file mode 100644 index 00000000..3493d858 --- /dev/null +++ b/packages/observability/test/observability.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from "bun:test"; +import { createWebVitalsHandler, MetricsRegistry } from "../src/index.ts"; + +describe("@wrnexus/observability", () => { + test("records deterministic counters and histograms", () => { + const registry = new MetricsRegistry(() => 123); + registry.increment("requests", 1, { route: "/" }); + registry.increment("requests", 2, { route: "/" }); + registry.observe("duration", 10); + registry.observe("duration", 20); + expect(registry.snapshot()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ name: "requests", value: 3 }), + expect.objectContaining({ name: "duration", value: 15, count: 2, min: 10, max: 20 }), + ]), + ); + }); + + test("accepts same-origin Web Vitals and rejects cross-site beacons", async () => { + const handler = createWebVitalsHandler(); + const good = await handler( + new Request("https://example.com/__wrnexus/metrics/vitals", { + method: "POST", + headers: { origin: "https://example.com", "content-type": "application/json" }, + body: JSON.stringify({ name: "LCP", value: 1200, route: "/" }), + }), + ); + expect(good.status).toBe(204); + const denied = await handler( + new Request("https://example.com/__wrnexus/metrics/vitals", { + method: "POST", + headers: { origin: "https://evil.example", "content-type": "application/json" }, + body: JSON.stringify({ name: "LCP", value: 1200 }), + }), + ); + expect(denied.status).toBe(403); + }); +}); diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 660daf59..1cbd8101 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/plugin", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 33947df2..155b740a 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/test/redis-driver.test.ts b/packages/pubsub/test/redis-driver.test.ts index 0a2b0476..4cd322cb 100644 --- a/packages/pubsub/test/redis-driver.test.ts +++ b/packages/pubsub/test/redis-driver.test.ts @@ -1,7 +1,7 @@ import { afterEach, expect, test } from "bun:test"; import { redisDriver } from "../src/redis.ts"; -const bun = globalThis.Bun as typeof Bun & { connect: (options: unknown) => Promise }; +const bun = Bun as typeof Bun & { connect: (options: unknown) => Promise }; const originalConnect = bun.connect; afterEach(() => { bun.connect = originalConnect; diff --git a/packages/queue/package.json b/packages/queue/package.json index 67ffddd0..888f5827 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 076810ab..eae6a1ab 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index bb82d823..9f52eacf 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/security/README.md b/packages/security/README.md new file mode 100644 index 00000000..8256c2f0 --- /dev/null +++ b/packages/security/README.md @@ -0,0 +1,11 @@ +# @wrnexus/security + +Secure-by-default utilities for WRNexusJS: bounded HTML-safe serialization, prototype-pollution rejection, URL policy, secure cookies, request hardening, security presets, and SSRF-safe remote fetches. + +```ts +import { safeFetch, securityPreset, setSecureCookie } from "@wrnexus/security"; + +export default { security: securityPreset("strict") }; +const response = await safeFetch(remoteUrl, { allowedHosts: ["api.example.com"] }); +setSecureCookie(ctx, "__Host-session", sessionId); +``` diff --git a/packages/security/package.json b/packages/security/package.json new file mode 100644 index 00000000..220a8f82 --- /dev/null +++ b/packages/security/package.json @@ -0,0 +1,16 @@ +{ + "name": "@wrnexus/security", + "version": "0.7.0", + "type": "module", + "description": "Security policies, safe serialization, SSRF protection, request limits, and secure cookie helpers for WRNexusJS.", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./fetch": "./src/fetch.ts", + "./serialization": "./src/serialization.ts", + "./trusted-html": "./src/trusted-html.ts" + }, + "dependencies": { + "@wrnexus/core": "workspace:*" + } +} diff --git a/packages/security/src/cookies.ts b/packages/security/src/cookies.ts new file mode 100644 index 00000000..5ce36202 --- /dev/null +++ b/packages/security/src/cookies.ts @@ -0,0 +1,55 @@ +import type { Context, CookieOptions } from "@wrnexus/core"; +import { SecurityError } from "./errors.ts"; + +export interface SecureCookieOptions extends CookieOptions { + hostOnly?: boolean; +} + +export function secureCookieOptions( + ctx: Pick, + options: SecureCookieOptions = {}, +): CookieOptions { + const secure = options.secure ?? ctx.url.protocol === "https:"; + const sameSite = options.sameSite ?? "Lax"; + if (sameSite.toString().toLowerCase() === "none" && !secure) { + throw new SecurityError( + "WRN-SEC-COOKIE-SAMESITE", + "SameSite=None cookies must also use Secure.", + ); + } + if (options.hostOnly && options.domain) { + throw new SecurityError("WRN-SEC-COOKIE-HOST", "Host-only cookies cannot set Domain."); + } + return { + path: options.path ?? "/", + ...options, + domain: options.hostOnly ? undefined : options.domain, + httpOnly: options.httpOnly ?? true, + secure, + sameSite, + }; +} + +export function setSecureCookie( + ctx: Pick, + name: string, + value: string, + options: SecureCookieOptions = {}, +): void { + if (name.startsWith("__Host-") && (options.domain || (options.path && options.path !== "/"))) { + throw new SecurityError( + "WRN-SEC-COOKIE-HOST-PREFIX", + "__Host- cookies require Path=/ and must not set Domain.", + ); + } + ctx.cookies.set( + name, + value, + secureCookieOptions(ctx, { + ...options, + hostOnly: name.startsWith("__Host-") || options.hostOnly, + path: name.startsWith("__Host-") ? "/" : options.path, + secure: name.startsWith("__Host-") ? true : options.secure, + }), + ); +} diff --git a/packages/security/src/errors.ts b/packages/security/src/errors.ts new file mode 100644 index 00000000..20b06a4a --- /dev/null +++ b/packages/security/src/errors.ts @@ -0,0 +1,11 @@ +export class SecurityError extends Error { + readonly code: string; + readonly status: number; + + constructor(code: string, message: string, status = 400, options?: ErrorOptions) { + super(message, options); + this.name = "SecurityError"; + this.code = code; + this.status = status; + } +} diff --git a/packages/security/src/fetch.ts b/packages/security/src/fetch.ts new file mode 100644 index 00000000..f49c1f06 --- /dev/null +++ b/packages/security/src/fetch.ts @@ -0,0 +1,160 @@ +import { lookup } from "node:dns/promises"; +import { isIP } from "node:net"; +import { SecurityError } from "./errors.ts"; +import { validateUrl, type SafeUrlPolicy } from "./url.ts"; + +export interface SafeFetchOptions extends RequestInit, SafeUrlPolicy { + timeoutMs?: number; + maxRedirects?: number; + maxResponseBytes?: number; + blockPrivateNetworks?: boolean; + /** Forward Authorization, Cookie, and Proxy-Authorization across origin-changing redirects. Defaults to false. */ + forwardSensitiveHeaders?: boolean; + resolver?: (hostname: string) => Promise; +} + +function isPrivateIpv4(address: string): boolean { + const parts = address.split(".").map(Number); + if ( + parts.length !== 4 || + parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255) + ) { + return false; + } + const [a, b] = parts; + return ( + a === 0 || + a === 10 || + a === 127 || + (a === 169 && b === 254) || + (a === 172 && b! >= 16 && b! <= 31) || + (a === 192 && b === 168) || + (a === 100 && b! >= 64 && b! <= 127) || + a! >= 224 + ); +} + +function isPrivateIpv6(address: string): boolean { + const normalized = address.toLowerCase().split("%")[0]!; + const mappedIpv4 = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(normalized)?.[1]; + if (mappedIpv4) return isPrivateIpv4(mappedIpv4); + return ( + normalized === "::" || + normalized === "::1" || + normalized.startsWith("fc") || + normalized.startsWith("fd") || + normalized.startsWith("fe8") || + normalized.startsWith("fe9") || + normalized.startsWith("fea") || + normalized.startsWith("feb") || + normalized.startsWith("ff") + ); +} + +export function isPrivateAddress(address: string): boolean { + const version = isIP(address); + return version === 4 ? isPrivateIpv4(address) : version === 6 ? isPrivateIpv6(address) : false; +} + +async function defaultResolver(hostname: string): Promise { + if (isIP(hostname)) return [hostname]; + return (await lookup(hostname, { all: true, verbatim: true })).map((entry) => entry.address); +} + +async function assertPublicHost(url: URL, options: SafeFetchOptions): Promise { + if (options.blockPrivateNetworks === false) return; + const addresses = await (options.resolver ?? defaultResolver)(url.hostname); + if (!addresses.length) { + throw new SecurityError("WRN-SEC-SSRF-DNS", `Host '${url.hostname}' did not resolve.`); + } + const blocked = addresses.find(isPrivateAddress); + if (blocked) { + throw new SecurityError( + "WRN-SEC-SSRF-PRIVATE", + `Host '${url.hostname}' resolves to blocked address '${blocked}'.`, + 403, + ); + } +} + +export async function safeFetch( + input: string | URL, + options: SafeFetchOptions = {}, +): Promise { + const timeoutMs = options.timeoutMs ?? 10_000; + const maxRedirects = options.maxRedirects ?? 3; + const maxResponseBytes = options.maxResponseBytes ?? 5 * 1024 * 1024; + const controller = new AbortController(); + const externalSignal = options.signal; + const abort = () => controller.abort(externalSignal?.reason); + externalSignal?.addEventListener("abort", abort, { once: true }); + const timer = setTimeout( + () => controller.abort(new Error("WRNexus safeFetch timeout")), + timeoutMs, + ); + + const init: RequestInit = { ...options, signal: controller.signal, redirect: "manual" }; + delete (init as Record).timeoutMs; + delete (init as Record).maxRedirects; + delete (init as Record).maxResponseBytes; + delete (init as Record).blockPrivateNetworks; + delete (init as Record).resolver; + delete (init as Record).forwardSensitiveHeaders; + delete (init as Record).base; + delete (init as Record).allowRelative; + delete (init as Record).allowedProtocols; + delete (init as Record).allowedHosts; + delete (init as Record).blockedHosts; + delete (init as Record).allowCredentials; + delete (init as Record).allowDataImages; + + try { + let current = validateUrl(input, { + ...options, + allowRelative: false, + allowedProtocols: options.allowedProtocols ?? ["https:"], + }); + let previousOrigin = current.origin; + for (let redirect = 0; ; redirect++) { + await assertPublicHost(current, options); + const requestInit: RequestInit = { ...init }; + if (!options.forwardSensitiveHeaders && current.origin !== previousOrigin) { + const headers = new Headers(init.headers); + headers.delete("authorization"); + headers.delete("cookie"); + headers.delete("proxy-authorization"); + requestInit.headers = headers; + } + const response = await fetch(current, requestInit); + if (response.status >= 300 && response.status < 400 && response.headers.has("location")) { + if (redirect >= maxRedirects) { + throw new SecurityError("WRN-SEC-SSRF-REDIRECT", "Too many redirects.", 502); + } + previousOrigin = current.origin; + current = validateUrl(new URL(response.headers.get("location")!, current), { + ...options, + allowRelative: false, + allowedProtocols: options.allowedProtocols ?? ["https:"], + }); + continue; + } + + const contentLength = Number(response.headers.get("content-length") ?? "0"); + if (Number.isFinite(contentLength) && contentLength > maxResponseBytes) { + throw new SecurityError("WRN-SEC-SSRF-SIZE", "Remote response is too large.", 502); + } + const bytes = new Uint8Array(await response.arrayBuffer()); + if (bytes.byteLength > maxResponseBytes) { + throw new SecurityError("WRN-SEC-SSRF-SIZE", "Remote response is too large.", 502); + } + return new Response(bytes, { + status: response.status, + statusText: response.statusText, + headers: response.headers, + }); + } + } finally { + clearTimeout(timer); + externalSignal?.removeEventListener("abort", abort); + } +} diff --git a/packages/security/src/index.ts b/packages/security/src/index.ts new file mode 100644 index 00000000..c349a2aa --- /dev/null +++ b/packages/security/src/index.ts @@ -0,0 +1,18 @@ +export { SecurityError } from "./errors.ts"; +export { assertSafeObject, isDangerousObjectKey, safeMerge } from "./object.ts"; +export type { SafeObjectOptions } from "./object.ts"; +export { isSafeUrl, sanitizeUrl, validateUrl } from "./url.ts"; +export type { SafeUrlPolicy } from "./url.ts"; +export { secureJsonStringify, serializeForHtml } from "./serialization.ts"; +export type { SecureSerializeOptions } from "./serialization.ts"; +export { secureCookieOptions, setSecureCookie } from "./cookies.ts"; +export type { SecureCookieOptions } from "./cookies.ts"; +export { requestHardening } from "./request.ts"; +export type { RequestHardeningOptions } from "./request.ts"; +export { safeFetch, isPrivateAddress } from "./fetch.ts"; +export type { SafeFetchOptions } from "./fetch.ts"; +export { securityPreset } from "./presets.ts"; +export type { SecurityPreset } from "./presets.ts"; + +export { createTrustedHtml, isTrustedHtml, unwrapTrustedHtml } from "./trusted-html.ts"; +export type { TrustedHtmlPolicy, TrustedHtmlValue } from "./trusted-html.ts"; diff --git a/packages/security/src/object.ts b/packages/security/src/object.ts new file mode 100644 index 00000000..fd675d12 --- /dev/null +++ b/packages/security/src/object.ts @@ -0,0 +1,94 @@ +import { SecurityError } from "./errors.ts"; + +const DANGEROUS_KEYS = new Set(["__proto__", "prototype", "constructor"]); + +export interface SafeObjectOptions { + maxDepth?: number; + maxKeys?: number; + allowInstances?: boolean; +} + +export function isDangerousObjectKey(key: string): boolean { + return DANGEROUS_KEYS.has(key); +} + +export function assertSafeObject(value: unknown, options: SafeObjectOptions = {}): void { + const maxDepth = options.maxDepth ?? 32; + const maxKeys = options.maxKeys ?? 10_000; + const seen = new WeakSet(); + let keyCount = 0; + + const visit = (current: unknown, depth: number): void => { + if (current === null || typeof current !== "object") return; + if (depth > maxDepth) { + throw new SecurityError("WRN-SEC-OBJECT-DEPTH", `Object depth exceeds ${maxDepth}.`, 413); + } + if (seen.has(current)) { + throw new SecurityError("WRN-SEC-OBJECT-CYCLE", "Cyclic objects are not accepted."); + } + seen.add(current); + + const proto = Object.getPrototypeOf(current); + if ( + !options.allowInstances && + !Array.isArray(current) && + proto !== Object.prototype && + proto !== null && + !(current instanceof Date) && + !(current instanceof URL) + ) { + throw new SecurityError( + "WRN-SEC-OBJECT-PROTOTYPE", + "Only plain objects, arrays, dates, and URLs are accepted.", + ); + } + + for (const key of Object.keys(current)) { + keyCount++; + if (keyCount > maxKeys) { + throw new SecurityError( + "WRN-SEC-OBJECT-KEYS", + `Object contains more than ${maxKeys} keys.`, + 413, + ); + } + if (isDangerousObjectKey(key)) { + throw new SecurityError( + "WRN-SEC-PROTOTYPE-POLLUTION", + `Dangerous object key '${key}' is not allowed.`, + ); + } + visit((current as Record)[key], depth + 1); + } + seen.delete(current); + }; + + visit(value, 0); +} + +export function safeMerge>( + target: T, + ...sources: Array | undefined | null> +): T { + for (const source of sources) { + if (!source) continue; + assertSafeObject(source); + for (const [key, value] of Object.entries(source)) { + if (isDangerousObjectKey(key)) continue; + const existing = target[key]; + if ( + value && + existing && + typeof value === "object" && + typeof existing === "object" && + !Array.isArray(value) && + !Array.isArray(existing) + ) { + safeMerge(existing as Record, value as Record); + } else { + target[key as keyof T] = value as T[keyof T]; + } + } + } + return target; +} diff --git a/packages/security/src/presets.ts b/packages/security/src/presets.ts new file mode 100644 index 00000000..2a9e9f36 --- /dev/null +++ b/packages/security/src/presets.ts @@ -0,0 +1,42 @@ +import type { SecurityConfig } from "@wrnexus/core"; + +export type SecurityPreset = "balanced" | "strict" | "api"; + +export function securityPreset(preset: SecurityPreset = "balanced"): SecurityConfig { + if (preset === "api") { + return { + contentSecurityPolicy: false, + frameOptions: "DENY", + crossOriginOpenerPolicy: "same-origin", + referrerPolicy: "no-referrer", + permissionsPolicy: {}, + extraHeaders: { + "Cross-Origin-Resource-Policy": "same-origin", + "Cache-Control": "no-store", + }, + }; + } + if (preset === "strict") { + return { + contentSecurityPolicy: { + useDefaults: true, + directives: { + "style-src": ["'self'"], + "script-src": ["'self'"], + "upgrade-insecure-requests": [], + }, + }, + trustedTypes: { enabled: true, policyNames: ["wrnexus"], requireForScript: true }, + hsts: { enabled: true, maxAge: 63_072_000, includeSubDomains: true, preload: true }, + extraHeaders: { + "Cross-Origin-Resource-Policy": "same-origin", + "Origin-Agent-Cluster": "?1", + }, + }; + } + return { + contentSecurityPolicy: { useDefaults: true }, + trustedTypes: { enabled: true, requireForScript: true }, + extraHeaders: { "Cross-Origin-Resource-Policy": "same-origin" }, + }; +} diff --git a/packages/security/src/request.ts b/packages/security/src/request.ts new file mode 100644 index 00000000..f32506de --- /dev/null +++ b/packages/security/src/request.ts @@ -0,0 +1,79 @@ +import type { Context, Middleware, RequestLimitsConfig } from "@wrnexus/core"; + +export type RequestHardeningOptions = RequestLimitsConfig; + +const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]); + +function headerBytes(headers: Headers): { count: number; bytes: number } { + let count = 0; + let bytes = 0; + headers.forEach((value, name) => { + count++; + bytes += new TextEncoder().encode(`${name}:${value}\r\n`).byteLength; + }); + return { count, bytes }; +} + +function hostAllowed(host: string, rules: string[]): boolean { + const normalized = host.toLowerCase().split(":")[0]!; + return rules.some((rule) => { + const candidate = rule.toLowerCase(); + return candidate.startsWith("*.") + ? normalized.endsWith(candidate.slice(1)) + : normalized === candidate; + }); +} + +function reject(status: number, message: string): Response { + return new Response(message, { + status, + headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }, + }); +} + +export function requestHardening(options: RequestHardeningOptions = {}): Middleware { + const maxUrlLength = options.maxUrlLength ?? 8_192; + const maxHeaderCount = options.maxHeaderCount ?? 100; + const maxHeaderBytes = options.maxHeaderBytes ?? 32 * 1024; + const maxQueryParameters = options.maxQueryParameters ?? 200; + const maxBodyBytes = options.maxBodyBytes ?? 10 * 1024 * 1024; + const timeoutMs = options.timeoutMs ?? 30_000; + const maxConcurrent = options.maxConcurrent ?? 1_000; + let concurrent = 0; + + return async (ctx: Context, next) => { + if (ctx.req.url.length > maxUrlLength) return reject(414, "URI Too Long"); + const measured = headerBytes(ctx.req.headers); + if (measured.count > maxHeaderCount || measured.bytes > maxHeaderBytes) { + return reject(431, "Request Header Fields Too Large"); + } + if ([...ctx.url.searchParams].length > maxQueryParameters) { + return reject(400, "Too many query parameters"); + } + const contentLength = Number(ctx.req.headers.get("content-length") ?? "0"); + if (Number.isFinite(contentLength) && contentLength > maxBodyBytes) { + return reject(413, "Payload Too Large"); + } + if (options.trustedHosts?.length) { + const host = ctx.req.headers.get("host") ?? ctx.url.host; + if (!hostAllowed(host, options.trustedHosts)) return reject(421, "Misdirected Request"); + } + if (options.fetchMetadata !== false && !SAFE_METHODS.has(ctx.req.method.toUpperCase())) { + const site = ctx.req.headers.get("sec-fetch-site"); + if (site === "cross-site") return reject(403, "Cross-site request denied"); + } + if (concurrent >= maxConcurrent) return reject(503, "Server Busy"); + + concurrent++; + let timer: ReturnType | undefined; + try { + const timeout = new Promise((resolve) => { + timer = setTimeout(() => resolve(reject(504, "Request Timeout")), timeoutMs); + }); + return await Promise.race([Promise.resolve(next()), timeout]); + } finally { + if (timer) clearTimeout(timer); + concurrent--; + } + }; +} diff --git a/packages/security/src/serialization.ts b/packages/security/src/serialization.ts new file mode 100644 index 00000000..895c4c75 --- /dev/null +++ b/packages/security/src/serialization.ts @@ -0,0 +1,66 @@ +import { SecurityError } from "./errors.ts"; +import { assertSafeObject, isDangerousObjectKey } from "./object.ts"; + +export interface SecureSerializeOptions { + maxDepth?: number; + maxKeys?: number; + maxBytes?: number; + redact?: RegExp | string[]; + redactedValue?: string; +} + +const DEFAULT_SECRET = + /(?:password|passwd|secret|token|api[-_]?key|private[-_]?key|otp|authorization|cookie)/i; + +function shouldRedact(key: string, rule: SecureSerializeOptions["redact"]): boolean { + if (Array.isArray(rule)) return rule.includes(key); + return (rule ?? DEFAULT_SECRET).test(key); +} + +export function secureJsonStringify(value: unknown, options: SecureSerializeOptions = {}): string { + assertSafeObject(value, { maxDepth: options.maxDepth, maxKeys: options.maxKeys }); + const redactedValue = options.redactedValue ?? "[REDACTED]"; + + const json = JSON.stringify(value, function secureReplacer(key, current) { + if (isDangerousObjectKey(key)) { + throw new SecurityError( + "WRN-SEC-PROTOTYPE-POLLUTION", + `Dangerous key '${key}' is not allowed.`, + ); + } + if (key && shouldRedact(key, options.redact)) return redactedValue; + if (typeof current === "bigint") return current.toString(); + if (typeof current === "function" || typeof current === "symbol") { + throw new SecurityError( + "WRN-SEC-SERIALIZE-TYPE", + `Cannot serialize ${typeof current} values.`, + ); + } + return current; + }); + + if (json === undefined) { + throw new SecurityError("WRN-SEC-SERIALIZE-EMPTY", "Value cannot be serialized."); + } + + const safe = json + .replace(/&/g, "\\u0026") + .replace(//g, "\\u003e") + .replace(/\u2028/g, "\\u2028") + .replace(/\u2029/g, "\\u2029"); + const bytes = new TextEncoder().encode(safe).byteLength; + const maxBytes = options.maxBytes ?? 256 * 1024; + if (bytes > maxBytes) { + throw new SecurityError( + "WRN-SEC-SERIALIZE-SIZE", + `Serialized payload exceeds ${maxBytes} bytes.`, + 413, + ); + } + return safe; +} + +export function serializeForHtml(value: unknown, options: SecureSerializeOptions = {}): string { + return secureJsonStringify(value, options); +} diff --git a/packages/security/src/trusted-html.ts b/packages/security/src/trusted-html.ts new file mode 100644 index 00000000..bc6823d7 --- /dev/null +++ b/packages/security/src/trusted-html.ts @@ -0,0 +1,52 @@ +import { SecurityError } from "./errors.ts"; + +const TRUSTED_HTML = Symbol("wrnexus.trusted-html"); + +export interface TrustedHtmlPolicy { + /** Stable policy name used in diagnostics and CSP/Trusted Types integration. */ + name: string; + /** Application-supplied, reviewed sanitizer. It must return sanitized HTML. */ + sanitize(input: string): string; +} + +export interface TrustedHtmlValue { + readonly policy: string; + readonly value: string; + readonly [TRUSTED_HTML]: true; +} + +export function createTrustedHtml(input: string, policy: TrustedHtmlPolicy): TrustedHtmlValue { + if (!policy?.name?.trim() || typeof policy.sanitize !== "function") { + throw new SecurityError( + "WRN-SEC-TRUSTED-HTML-POLICY", + "Trusted HTML requires a named sanitizer policy.", + ); + } + const value = policy.sanitize(String(input)); + if (typeof value !== "string") { + throw new SecurityError( + "WRN-SEC-TRUSTED-HTML-RESULT", + `Trusted HTML policy '${policy.name}' must return a string.`, + ); + } + return Object.freeze({ policy: policy.name.trim(), value, [TRUSTED_HTML]: true as const }); +} + +export function isTrustedHtml(value: unknown): value is TrustedHtmlValue { + return Boolean( + value && + typeof value === "object" && + (value as Partial)[TRUSTED_HTML] === true && + typeof (value as Partial).value === "string", + ); +} + +export function unwrapTrustedHtml(value: TrustedHtmlValue): string { + if (!isTrustedHtml(value)) { + throw new SecurityError( + "WRN-SEC-TRUSTED-HTML-REQUIRED", + "Raw HTML must be produced by createTrustedHtml().", + ); + } + return value.value; +} diff --git a/packages/security/src/url.ts b/packages/security/src/url.ts new file mode 100644 index 00000000..118ab310 --- /dev/null +++ b/packages/security/src/url.ts @@ -0,0 +1,101 @@ +import { SecurityError } from "./errors.ts"; + +export interface SafeUrlPolicy { + base?: string | URL; + allowRelative?: boolean; + allowedProtocols?: string[]; + allowedHosts?: string[]; + blockedHosts?: string[]; + allowCredentials?: boolean; + allowDataImages?: boolean; +} + +const DEFAULT_PROTOCOLS = ["http:", "https:"]; + +function hasAsciiControlOrSpace(value: string): boolean { + for (const character of value) { + const code = character.charCodeAt(0); + if (code <= 0x20 || code === 0x7f) return true; + } + return false; +} + +function hostnameMatches(hostname: string, rule: string): boolean { + const normalized = rule.toLowerCase().replace(/\.$/, ""); + const host = hostname.toLowerCase().replace(/\.$/, ""); + if (normalized.startsWith("*.")) { + const suffix = normalized.slice(1); + return host.endsWith(suffix) && host.length > suffix.length; + } + return host === normalized; +} + +export function validateUrl(value: string | URL, policy: SafeUrlPolicy = {}): URL { + const raw = String(value); + if (!raw || hasAsciiControlOrSpace(raw)) { + throw new SecurityError( + "WRN-SEC-URL-CONTROL", + "URL contains whitespace or control characters.", + ); + } + + const isRelative = /^(?:\.{0,2}\/|\/|\?|#)/.test(raw); + if (isRelative && policy.allowRelative === false) { + throw new SecurityError("WRN-SEC-URL-RELATIVE", "Relative URLs are not allowed."); + } + + let url: URL; + try { + url = new URL(raw, policy.base ?? "http://wrnexus.invalid"); + } catch (error) { + throw new SecurityError("WRN-SEC-URL-INVALID", "Invalid URL.", 400, { cause: error }); + } + + if (url.protocol === "data:") { + if (policy.allowDataImages && /^data:image\/(?:png|gif|jpeg|webp|avif);/i.test(raw)) return url; + throw new SecurityError("WRN-SEC-URL-DATA", "Data URLs are not allowed by this policy."); + } + + const protocols = policy.allowedProtocols ?? DEFAULT_PROTOCOLS; + if (!protocols.includes(url.protocol)) { + throw new SecurityError( + "WRN-SEC-URL-PROTOCOL", + `URL protocol '${url.protocol}' is not allowed.`, + ); + } + if (!policy.allowCredentials && (url.username || url.password)) { + throw new SecurityError("WRN-SEC-URL-CREDENTIALS", "Credentials in URLs are not allowed."); + } + + if (policy.blockedHosts?.some((rule) => hostnameMatches(url.hostname, rule))) { + throw new SecurityError("WRN-SEC-URL-BLOCKED-HOST", `Host '${url.hostname}' is blocked.`); + } + if ( + policy.allowedHosts?.length && + !policy.allowedHosts.some((rule) => hostnameMatches(url.hostname, rule)) + ) { + throw new SecurityError("WRN-SEC-URL-HOST", `Host '${url.hostname}' is not allowlisted.`); + } + + return url; +} + +export function isSafeUrl(value: string | URL, policy: SafeUrlPolicy = {}): boolean { + try { + validateUrl(value, policy); + return true; + } catch { + return false; + } +} + +export function sanitizeUrl(value: unknown, policy: SafeUrlPolicy = {}): string { + try { + const raw = String(value ?? ""); + const url = validateUrl(raw, policy); + if (/^(?:\.{0,2}\/|\/|\?|#)/.test(raw)) return raw; + return url.toString(); + } catch { + return "about:blank"; + } +} diff --git a/packages/security/test/security.test.ts b/packages/security/test/security.test.ts new file mode 100644 index 00000000..a77038a8 --- /dev/null +++ b/packages/security/test/security.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from "bun:test"; +import { + assertSafeObject, + createTrustedHtml, + isPrivateAddress, + isTrustedHtml, + secureJsonStringify, + setSecureCookie, + unwrapTrustedHtml, + validateUrl, +} from "../src/index.ts"; + +describe("@wrnexus/security", () => { + test("escapes HTML-significant JSON and redacts secrets", () => { + const json = secureJsonStringify({ html: "", token: "secret", count: 1 }); + expect(json).toContain("\\u003c/script\\u003e"); + expect(json).toContain("[REDACTED]"); + expect(json).not.toContain("secret"); + }); + + test("rejects prototype-pollution keys and unsafe URL protocols", () => { + const unsafe = JSON.parse('{"__proto__":{"admin":true}}'); + expect(() => assertSafeObject(unsafe)).toThrow(); + expect(() => validateUrl("javascript:alert(1)")).toThrow(); + }); + + test("identifies private IP ranges", () => { + expect(isPrivateAddress("127.0.0.1")).toBe(true); + expect(isPrivateAddress("10.1.2.3")).toBe(true); + expect(isPrivateAddress("8.8.8.8")).toBe(false); + }); + + test("enforces __Host cookie rules", () => { + const writes: unknown[] = []; + const ctx = { + url: new URL("https://example.com"), + cookies: { set: (...args: unknown[]) => writes.push(args) }, + } as any; + setSecureCookie(ctx, "__Host-session", "value"); + expect(writes).toHaveLength(1); + expect(writes[0]).toEqual([ + "__Host-session", + "value", + expect.objectContaining({ secure: true, httpOnly: true, path: "/", domain: undefined }), + ]); + }); + + test("accepts repeated references while still rejecting cycles", () => { + const shared = { value: 1 }; + expect(() => assertSafeObject({ first: shared, second: shared })).not.toThrow(); + const cyclic: Record = {}; + cyclic.self = cyclic; + expect(() => assertSafeObject(cyclic)).toThrow(); + }); + + test("trusted HTML requires an explicit sanitizer policy", () => { + const value = createTrustedHtml('

Hello

', { + name: "test-policy", + sanitize: (input) => input.replace(/\s+onclick="[^"]*"/g, ""), + }); + expect(isTrustedHtml(value)).toBe(true); + expect(unwrapTrustedHtml(value)).toBe("

Hello

"); + expect(() => unwrapTrustedHtml({ value: "unsafe" } as any)).toThrow(); + }); +}); diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 47d4b44c..1dc69624 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { @@ -10,6 +10,7 @@ }, "dependencies": { "@wrnexus/core": "workspace:*", - "@wrnexus/store": "workspace:*" + "@wrnexus/store": "workspace:*", + "@wrnexus/security": "workspace:*" } } diff --git a/packages/ssr/src/store-context.ts b/packages/ssr/src/store-context.ts index 283eb519..7255f278 100644 --- a/packages/ssr/src/store-context.ts +++ b/packages/ssr/src/store-context.ts @@ -1,3 +1,4 @@ +import { serializeForHtml } from "@wrnexus/security"; import { createRequestStoreContainer } from "@wrnexus/store/server"; import type { StoreContainer } from "@wrnexus/store"; @@ -19,6 +20,6 @@ export async function disposeRequestStores(request: object): Promise { } export function renderStoreHydration(container: StoreContainer, nonce?: string): string { - const json = JSON.stringify(container.serialize()).replace(/${json}`; } diff --git a/packages/store/package.json b/packages/store/package.json index dbfa1146..9be4f6d6 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/store", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index a1999d1a..5edf3c49 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/src/audit.ts b/packages/styles/src/audit.ts index cedeaf84..b47b71b1 100644 --- a/packages/styles/src/audit.ts +++ b/packages/styles/src/audit.ts @@ -78,3 +78,76 @@ export function contrast(foreground: string, background: string): ContrastResult level: ratio >= 7 ? "aaa" : ratio >= 4.5 ? "aa" : ratio >= 3 ? "aa-large" : "fail", }; } + +export interface CssPerformanceAuditIssue { + code: string; + severity: "error" | "warning" | "info"; + message: string; + line?: number; +} + +function cssLineAt(source: string, offset: number): number { + return source.slice(0, offset).split(/\r?\n/).length; +} + +/** Detect CSS patterns that commonly increase style, paint, or compositing cost. */ +export function auditCssPerformance(source: string): CssPerformanceAuditIssue[] { + const issues: CssPerformanceAuditIssue[] = []; + const checks: Array<{ + pattern: RegExp; + code: string; + severity: CssPerformanceAuditIssue["severity"]; + message: string; + }> = [ + { + pattern: /transition\s*:\s*all\b/gi, + code: "WRN-CSS-TRANSITION-ALL", + severity: "warning", + message: "Avoid transition: all; list only properties that should animate.", + }, + { + pattern: /backdrop-filter\s*:\s*[^;]*(?:blur\((?:[3-9]\d|\d{3,})px\))/gi, + code: "WRN-CSS-BACKDROP-BLUR", + severity: "warning", + message: "Large backdrop blur areas can be expensive to composite.", + }, + { + pattern: /box-shadow\s*:[^;]*(?:,\s*[^;]+){4,}/gi, + code: "WRN-CSS-MANY-SHADOWS", + severity: "warning", + message: "Many layered shadows can increase paint cost.", + }, + { + pattern: /(?:^|[},])\s*\*\s*(?:[,{])/gm, + code: "WRN-CSS-UNIVERSAL-SELECTOR", + severity: "info", + message: "Review universal selectors used in large DOM subtrees.", + }, + ]; + for (const check of checks) { + for (const match of source.matchAll(check.pattern)) { + issues.push({ + code: check.code, + severity: check.severity, + message: check.message, + line: cssLineAt(source, match.index ?? 0), + }); + } + } + + const keyframes = new Map(); + for (const match of source.matchAll(/@keyframes\s+([A-Za-z_][\w-]*)/g)) { + const name = match[1]!; + keyframes.set(name, (keyframes.get(name) ?? 0) + 1); + } + for (const [name, count] of keyframes) { + if (count > 1) { + issues.push({ + code: "WRN-CSS-DUPLICATE-KEYFRAMES", + severity: "warning", + message: `@keyframes ${name} is declared ${count} times.`, + }); + } + } + return issues; +} diff --git a/packages/styles/src/config.ts b/packages/styles/src/config.ts index 3f5eb9be..a898cfb2 100644 --- a/packages/styles/src/config.ts +++ b/packages/styles/src/config.ts @@ -165,6 +165,10 @@ export interface ObservabilityConfig { sampleRate?: number; exporter?: "console" | "otlp" | "none"; endpoint?: string; + /** Collect privacy-preserving Core Web Vitals from real browsers. */ + webVitals?: boolean; + /** Same-origin endpoint receiving Web Vitals. */ + webVitalsEndpoint?: string; } export interface TenancyConfig { @@ -184,11 +188,13 @@ export interface BuildConfig { export interface NavigationConfig { /** - * `client` progressively enhances same-origin links with in-place page swaps. + * `auto` (default) omits navigation JavaScript from fully static pages and + * progressively enhances routes that already need browser behavior. + * `client` always enhances same-origin links with in-place page swaps. * `document` keeps normal browser navigation so every route performs a fresh * server-rendered document request. */ - mode?: "client" | "document"; + mode?: "auto" | "client" | "document"; } export interface ImportsConfig { diff --git a/packages/styles/src/index.ts b/packages/styles/src/index.ts index b119e930..8715b4b5 100644 --- a/packages/styles/src/index.ts +++ b/packages/styles/src/index.ts @@ -152,6 +152,12 @@ export { normalizeStyleSources, tailwindSourceDirectives, contrast, + auditCssPerformance, +} from "./audit.ts"; +export type { + CssTokenAudit, + StyleSource, + ContrastResult, + CssPerformanceAuditIssue, } from "./audit.ts"; -export type { CssTokenAudit, StyleSource, ContrastResult } from "./audit.ts"; export * from "./theme.ts"; diff --git a/packages/syntax/package.json b/packages/syntax/package.json index f845838f..56fa28c0 100644 --- a/packages/syntax/package.json +++ b/packages/syntax/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/syntax", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/syntax/src/diagnostics.ts b/packages/syntax/src/diagnostics.ts index a3dc5058..aebd85eb 100644 --- a/packages/syntax/src/diagnostics.ts +++ b/packages/syntax/src/diagnostics.ts @@ -32,6 +32,14 @@ export interface DiagnoseOptions { accessibility?: boolean; } +function stripAsciiControlAndSpace(value: string): string { + let result = ""; + for (const character of value) { + if (character.charCodeAt(0) > 0x20) result += character; + } + return result; +} + function maskJavaScriptTrivia(source: string): string { let result = ""; let index = 0; @@ -222,19 +230,76 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[] } let interactive = ast.states.length > 0 || ast.effects.length > 0 || ast.watches.length > 0; + const urlAttributes = new Set([ + "href", + "src", + "action", + "formaction", + "poster", + "cite", + "background", + "xlink:href", + ]); walk(ast.view, (node) => { - if (node.type === "element" && node.attrs.some((attribute) => attribute.event)) - interactive = true; - if (!options.accessibility || node.type !== "element") return; + if (node.type !== "element") return; + if (node.attrs.some((attribute) => attribute.event)) interactive = true; const tag = node.tag.toLowerCase(); - if (tag === "img" && !node.attrs.some((attribute) => attribute.name === "alt")) { - diagnostics.push({ - code: WRN_DIAGNOSTIC_CODES.accessibility, - severity: "warning", - message: "Image is missing an alt attribute.", - hint: 'Add alt text, or alt="" for a decorative image.', - file: options.file, - }); + + for (const attribute of node.attrs) { + if (attribute.event || attribute.boolean || !urlAttributes.has(attribute.name.toLowerCase())) + continue; + if (attribute.value.includes("{")) continue; + const value = stripAsciiControlAndSpace(attribute.value.trim()).toLowerCase(); + if ( + /^(?:javascript|vbscript|file):/.test(value) || + /^data:(?!image\/(?:png|gif|jpeg|webp|avif);)/.test(value) + ) { + diagnostics.push({ + code: "WRN-SEC-UNSAFE-URL", + severity: "error", + message: `Unsafe URL protocol in ${attribute.name} on <${node.tag}>.`, + hint: "Use a relative URL, https:, mailto:, tel:, or a framework-validated URL helper.", + file: options.file, + }); + } + } + + if (tag === "a") { + const target = node.attrs.find((attribute) => attribute.name === "target")?.value; + const rel = node.attrs.find((attribute) => attribute.name === "rel")?.value ?? ""; + if (target === "_blank" && !/\bnoopener\b/i.test(rel)) { + diagnostics.push({ + code: "WRN-SEC-BLANK-REL", + severity: "warning", + message: "A target=_blank link should include rel=noopener.", + hint: 'Add rel="noopener noreferrer".', + file: options.file, + }); + } + } + + if (!options.accessibility) return; + if (tag === "img") { + if (!node.attrs.some((attribute) => attribute.name === "alt")) { + diagnostics.push({ + code: WRN_DIAGNOSTIC_CODES.accessibility, + severity: "warning", + message: "Image is missing an alt attribute.", + hint: 'Add alt text, or alt="" for a decorative image.', + file: options.file, + }); + } + const hasWidth = node.attrs.some((attribute) => attribute.name === "width"); + const hasHeight = node.attrs.some((attribute) => attribute.name === "height"); + if (!hasWidth || !hasHeight) { + diagnostics.push({ + code: "WRN-PERF-IMAGE-DIMENSIONS", + severity: "warning", + message: "Image width and height are required to prevent layout shifts.", + hint: "Declare intrinsic width and height, or use @wrnexus/image.", + file: options.file, + }); + } } }); if (ast.runtime === "server" && interactive) { @@ -291,6 +356,29 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[] file: options.file, }); } + if ( + fn.runtime !== "server" && + /\b(?:eval\s*\(|new\s+Function\s*\(|document\.write\s*\(|\.innerHTML\s*=|\.outerHTML\s*=|insertAdjacentHTML\s*\()/.test( + fn.body, + ) + ) { + diagnostics.push({ + code: "WRN-SEC-DOM-SINK", + severity: "error", + message: `Client function '${fn.name}' uses an unsafe dynamic-code or HTML sink.`, + hint: "Use compiled templates, textContent, typed outputs, or a reviewed TrustedHTML sanitizer.", + file: options.file, + }); + } + if (fn.runtime !== "server" && /\b(?:setTimeout|setInterval)\s*\(\s*["'`]/.test(fn.body)) { + diagnostics.push({ + code: "WRN-SEC-STRING-TIMER", + severity: "error", + message: `Client function '${fn.name}' passes a string to a timer.`, + hint: "Pass a function instead of executable text.", + file: options.file, + }); + } const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name)); for (const prop of ast.props) { if (containsReadonlyPropMutation(fn.body, prop.name, parameterNames)) { @@ -317,6 +405,18 @@ function astDiagnostics(ast: PageAst, options: DiagnoseOptions): WrnDiagnostic[] file: options.file, }); } + if ( + state.runtime !== "server" && + /\b(?:process\.env|Bun\.env|Deno\.env|ctx\.env|import\.meta\.env)\b/.test(state.expr) + ) { + diagnostics.push({ + code: "WRN-SEC-SERVER-SECRET-SOURCE", + severity: "error", + message: `Browser-visible state '${state.name}' reads from a server environment source.`, + hint: "Move environment-backed values into server state and return only an explicitly safe result.", + file: options.file, + }); + } } if (ast.persist) { const stateNames = new Set( diff --git a/packages/test/package.json b/packages/test/package.json index 8951b87b..a684f6e1 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 818e0fd0..59d0478b 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/typecheck/package.json b/packages/typecheck/package.json index c49b83c8..6ab46eb4 100644 --- a/packages/typecheck/package.json +++ b/packages/typecheck/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/typecheck", - "version": "0.6.0", + "version": "0.7.0", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ui/package.json b/packages/ui/package.json index bde654cb..e8762a38 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 5136c198..bf4678d5 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index 9b3e898e..3fdd404f 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/test/validation.test.ts b/packages/validation/test/validation.test.ts index d96db958..dcc9bc72 100644 --- a/packages/validation/test/validation.test.ts +++ b/packages/validation/test/validation.test.ts @@ -1,7 +1,21 @@ import { test, expect } from "bun:test"; -import { Window } from "happy-dom"; +import { + Window, + type Event as HappyDOMEvent, + type HTMLFormElement as HappyDOMHTMLFormElement, + type HTMLInputElement as HappyDOMHTMLInputElement, + type HTMLElement as HappyDOMHTMLElement, + type IEventInit, +} from "happy-dom"; import { v, invalid, parseBody, checkField, parseEnv, VALIDATE_RUNTIME } from "../src/index.ts"; +type HappyDOMEventConstructor = new (type: string, init?: IEventInit) => HappyDOMEvent; + +function windowEvent(win: Window, type: string, init?: IEventInit): HappyDOMEvent { + const EventConstructor = (win as unknown as { Event: HappyDOMEventConstructor }).Event; + return new EventConstructor(type, init); +} + const login = v.object({ email: v.string().email(), password: v.string().min(8, "too short"), @@ -69,7 +83,7 @@ test("browser validation displays the serialized custom required message", () => const runtime = win.__wireValidate as { init(root: Document): void }; runtime.init(win.document as unknown as Document); const form = win.document.querySelector("form")!; - win.document.querySelector("input")!.dispatchEvent(new win.Event("blur", { bubbles: true })); + win.document.querySelector("input")!.dispatchEvent(windowEvent(win, "blur", { bubbles: true })); expect(form.noValidate).toBe(true); expect(form.hasAttribute("novalidate")).toBe(true); @@ -257,7 +271,7 @@ test("validation can bind after a package registers a missing schema", () => { runtime.init(win.document as unknown as Document); win.document .querySelector("form")! - .dispatchEvent(new win.Event("submit", { bubbles: true, cancelable: true })); + .dispatchEvent(windowEvent(win, "submit", { bubbles: true, cancelable: true })); expect(win.document.querySelector("[data-error=identifier]")!.textContent).toBe( "Enter an identifier", ); @@ -286,17 +300,17 @@ test("schema-backed forms validate on input, change, and blur and synchronize fi try { (0, eval)(VALIDATE_RUNTIME); - const input = win.document.querySelector("input") as unknown as HTMLInputElement; - const field = win.document.querySelector(".wire-next--field") as unknown as HTMLElement; - const error = win.document.querySelector("[data-error=email]") as unknown as HTMLElement; + const input = win.document.querySelector("input") as HappyDOMHTMLInputElement; + const field = win.document.querySelector(".wire-next--field") as HappyDOMHTMLElement; + const error = win.document.querySelector("[data-error=email]") as HappyDOMHTMLElement; - input.dispatchEvent(new win.Event("input", { bubbles: true }) as unknown as Event); + input.dispatchEvent(windowEvent(win, "input", { bubbles: true })); expect(error.textContent).toBe("Enter an email"); expect(input.getAttribute("aria-invalid")).toBe("true"); expect(field.dataset.invalid).toBe("true"); input.value = "person@example.com"; - input.dispatchEvent(new win.Event("change", { bubbles: true }) as unknown as Event); + input.dispatchEvent(windowEvent(win, "change", { bubbles: true })); expect(error.textContent).toBe(""); expect(input.hasAttribute("aria-invalid")).toBe(false); expect(field.dataset.invalid).toBe("false"); @@ -321,7 +335,7 @@ test("submit guards run only after schema fields are valid", () => { try { (0, eval)(VALIDATE_RUNTIME); - const form = win.document.querySelector("form")! as unknown as HTMLFormElement & { + const form = win.document.querySelector("form")! as HappyDOMHTMLFormElement & { __wireSubmitGuards?: Array<() => boolean>; }; let guardCalls = 0; @@ -332,16 +346,12 @@ test("submit guards run only after schema fields are valid", () => { }, ]; - form.dispatchEvent( - new win.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event, - ); + form.dispatchEvent(windowEvent(win, "submit", { bubbles: true, cancelable: true })); expect(guardCalls).toBe(0); expect(win.document.querySelector("[data-error=email]")!.textContent).toBe("Enter an email"); - (form.elements.namedItem("email") as HTMLInputElement).value = "person@example.com"; - form.dispatchEvent( - new win.Event("submit", { bubbles: true, cancelable: true }) as unknown as Event, - ); + (form.elements.namedItem("email") as HappyDOMHTMLInputElement).value = "person@example.com"; + form.dispatchEvent(windowEvent(win, "submit", { bubbles: true, cancelable: true })); expect(guardCalls).toBe(1); } finally { delete (globalThis as Record).window; diff --git a/scripts/benchmark-framework.mjs b/scripts/benchmark-framework.mjs new file mode 100644 index 00000000..117bb643 --- /dev/null +++ b/scripts/benchmark-framework.mjs @@ -0,0 +1,76 @@ +import console from "node:console"; +import { readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import process from "node:process"; +import { runBenchmark, assertBenchmarkBudget } from "../packages/benchmark/src/index.ts"; +import { TagCache } from "../packages/cache/src/memory.ts"; +import { secureJsonStringify } from "../packages/security/src/serialization.ts"; +import { parse } from "../packages/syntax/src/index.ts"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const componentSource = readFileSync( + join(root, "packages", "ui", "components", "button.wrn"), + "utf8", +); +const hydrationPayload = { + route: "/benchmark", + user: { id: "user-1", displayName: "WRNexus", token: "must-redact" }, + items: Array.from({ length: 100 }, (_, index) => ({ id: index, label: `Item ${index}` })), +}; +const cache = new TagCache({ maxEntries: 1000 }); +for (let index = 0; index < 500; index++) cache.set(`item:${index}`, index, { tags: ["items"] }); + +const benchmarks = [ + { + result: await runBenchmark("syntax-parse-component", () => parse(componentSource), { + iterations: 100, + warmup: 10, + }), + budget: { maxAbsoluteMs: 50, minOperationsPerSecond: 20 }, + }, + { + result: await runBenchmark( + "secure-hydration-serialization", + () => secureJsonStringify(hydrationPayload), + { iterations: 250, warmup: 20 }, + ), + budget: { maxAbsoluteMs: 20, minOperationsPerSecond: 100 }, + }, + { + result: await runBenchmark( + "tag-cache-read-100", + () => { + for (let index = 0; index < 100; index++) cache.get(`item:${index}`); + }, + { iterations: 250, warmup: 20 }, + ), + budget: { maxAbsoluteMs: 20, minOperationsPerSecond: 100 }, + }, +]; + +for (const benchmark of benchmarks) { + assertBenchmarkBudget(benchmark.result, undefined, benchmark.budget); +} + +const report = { + schemaVersion: 1, + frameworkVersion: "0.7.0", + environment: { runtime: process.version, platform: process.platform, arch: process.arch }, + benchmarks: benchmarks.map(({ result, budget }) => ({ result, budget })), +}; +const reportDir = join(root, ".wrnexus", "reports"); +mkdirSync(reportDir, { recursive: true }); +writeFileSync(join(reportDir, "benchmark-0.7.0.json"), `${JSON.stringify(report, null, 2)}\n`); +console.log( + JSON.stringify( + report.benchmarks.map(({ result }) => ({ + name: result.name, + meanMs: result.meanMs, + p95Ms: result.p95Ms, + operationsPerSecond: result.operationsPerSecond, + })), + null, + 2, + ), +); diff --git a/scripts/generate-sbom.mjs b/scripts/generate-sbom.mjs new file mode 100644 index 00000000..85448a4b --- /dev/null +++ b/scripts/generate-sbom.mjs @@ -0,0 +1,112 @@ +import console from "node:console"; +import { createHash } from "node:crypto"; +import { readFileSync, readdirSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const rootManifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); +const components = []; + +function npmPurl(name, version) { + if (name.startsWith("@")) { + const slash = name.indexOf("/"); + return `pkg:npm/%40${encodeURIComponent(name.slice(1, slash))}/${encodeURIComponent(name.slice(slash + 1))}@${version}`; + } + return `pkg:npm/${encodeURIComponent(name)}@${version}`; +} + +for (const entry of readdirSync(join(root, "packages"), { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const file = join(root, "packages", entry.name, "package.json"); + try { + const manifest = JSON.parse(readFileSync(file, "utf8")); + if (!manifest.name?.startsWith("@wrnexus/")) continue; + components.push({ + type: "library", + name: manifest.name, + version: manifest.version, + "bom-ref": npmPurl(manifest.name, manifest.version), + purl: npmPurl(manifest.name, manifest.version), + properties: [ + { name: "wrnexus:workspace", value: "true" }, + { name: "wrnexus:private", value: String(manifest.publishConfig?.access !== "public") }, + ], + }); + } catch { + // Not a workspace package. + } +} + +// Bun's text lockfile is JSON with optional trailing commas. Package tuples use +// ["name@version", resolved, metadata, integrity], which is enough to include +// every resolved transitive dependency without requiring node_modules. +try { + const lockSource = readFileSync(join(root, "bun.lock"), "utf8").replace(/,\s*([}\]])/g, "$1"); + const lock = JSON.parse(lockSource); + const external = new Map(); + for (const value of Object.values(lock.packages ?? {})) { + if (!Array.isArray(value) || typeof value[0] !== "string") continue; + const specifier = value[0]; + const split = specifier.lastIndexOf("@"); + if (split <= 0) continue; + const name = specifier.slice(0, split); + const version = specifier.slice(split + 1); + if (!name || !version || name.startsWith("@wrnexus/")) continue; + const ref = npmPurl(name, version); + const component = { + type: "library", + name, + version, + "bom-ref": ref, + purl: ref, + properties: [{ name: "wrnexus:dependency-source", value: "bun.lock" }], + }; + const integrity = typeof value[3] === "string" ? value[3] : ""; + if (integrity.startsWith("sha512-")) { + component.hashes = [{ alg: "SHA-512", content: integrity.slice("sha512-".length) }]; + } + external.set(ref, component); + } + components.push(...external.values()); +} catch (error) { + throw new Error( + `Unable to parse bun.lock for the SBOM: ${error instanceof Error ? error.message : String(error)}`, + { cause: error }, + ); +} + +components.sort((a, b) => { + const left = `${a.name}@${a.version}`; + const right = `${b.name}@${b.version}`; + return left < right ? -1 : left > right ? 1 : 0; +}); + +function deterministicUuid(value) { + const hash = createHash("sha256").update(value).digest("hex").slice(0, 32).split(""); + hash[12] = "5"; + hash[16] = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16); + const hex = hash.join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +const version = String(rootManifest.version); +const identity = JSON.stringify({ framework: "WRNexusJS", version, components }); +const document = { + bomFormat: "CycloneDX", + specVersion: "1.5", + serialNumber: `urn:uuid:${deterministicUuid(identity)}`, + version: 1, + metadata: { + component: { + type: "framework", + name: "WRNexusJS", + version, + "bom-ref": `pkg:generic/WRNexusJS@${version}`, + }, + }, + components, +}; +const file = join(root, `SBOM-${version}.cdx.json`); +writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`); +console.log(`Generated ${file} with ${components.length} workspace and transitive components.`); diff --git a/scripts/lib/package-integrity.ts b/scripts/lib/package-integrity.ts new file mode 100644 index 00000000..cc3d3dbe --- /dev/null +++ b/scripts/lib/package-integrity.ts @@ -0,0 +1,76 @@ +import { createHash } from "node:crypto"; +import { readdirSync, readFileSync } from "node:fs"; +import { basename, join, relative } from "node:path"; + +export interface StagedFileIntegrity { + path: string; + bytes: number; + sha256: string; +} + +export interface StagedPackageIntegrity { + name: string; + version: string; + files: StagedFileIntegrity[]; +} + +const forbiddenStageName = + /^(?:\.env(?:\..*)?|focus-shims\.d\.ts|tsconfig\.focus\.json|id_rsa|id_ed25519|.*\.(?:pem|key|p12|pfx))$/i; +const highConfidenceSecret = + /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b|\bnpm_[A-Za-z0-9]{30,}\b|\bgh[pousr]_[A-Za-z0-9]{30,}\b/; + +function listFiles(root: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(root, { recursive: true, withFileTypes: true })) { + if (!entry.isFile()) continue; + files.push(join(entry.parentPath, entry.name)); + } + return files.sort((a, b) => { + const left = relative(root, a).replace(/\\/g, "/"); + const right = relative(root, b).replace(/\\/g, "/"); + return left < right ? -1 : left > right ? 1 : 0; + }); +} + +export function validateAndHashStage( + stage: string, + manifest: Record, +): StagedPackageIntegrity { + const name = String(manifest.name ?? "unknown package"); + const files = listFiles(stage); + if (!files.length) throw new Error(`${name} staged no files.`); + const integrity: StagedFileIntegrity[] = []; + + for (const file of files) { + const path = relative(stage, file).replace(/\\/g, "/"); + const filename = basename(file); + if (forbiddenStageName.test(filename)) { + throw new Error(`${name} staged forbidden secret-like file ${path}.`); + } + if (path.endsWith(".map")) { + throw new Error(`${name} staged source map ${path}; package source maps are disabled.`); + } + if (path.endsWith(".ts") && !path.endsWith(".d.ts")) { + throw new Error(`${name} staged TypeScript source ${path}.`); + } + + const value = readFileSync(file); + if (value.byteLength <= 2 * 1024 * 1024) { + const source = value.toString("utf8"); + if (highConfidenceSecret.test(source)) { + throw new Error(`${name} staged content matching a high-confidence secret in ${path}.`); + } + } + integrity.push({ + path, + bytes: value.byteLength, + sha256: createHash("sha256").update(value).digest("hex"), + }); + } + + return { + name, + version: String(manifest.version ?? "unknown"), + files: integrity, + }; +} diff --git a/scripts/publish-packages.ts b/scripts/publish-packages.ts index 9dec78d5..7551d44b 100644 --- a/scripts/publish-packages.ts +++ b/scripts/publish-packages.ts @@ -26,6 +26,8 @@ import { } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import process from "node:process"; +import { validateAndHashStage, type StagedPackageIntegrity } from "./lib/package-integrity.ts"; const repoRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); const packagesDir = join(repoRoot, "packages"); @@ -240,6 +242,7 @@ async function main() { mkdirSync(stageRoot, { recursive: true }); const order: string[] = []; + const packageIntegrity: StagedPackageIntegrity[] = []; for (const p of pkgs) { const entries = entriesOf(p.dir, p.manifest); process.stdout.write(`▸ ${p.name} … `); @@ -297,11 +300,27 @@ async function main() { ); } + packageIntegrity.push(validateAndHashStage(stage, manifest)); order.push(p.name); - console.log("staged"); + console.log("staged + verified"); } - console.log(`\n✓ Staged ${order.length} package(s) under .publish/ (access: ${ACCESS})`); + const integrityDocument = { + schemaVersion: 1, + frameworkVersion: String( + JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version, + ), + access: ACCESS, + packages: packageIntegrity, + }; + writeFileSync( + join(stageRoot, "PACKAGE-INTEGRITY.json"), + `${JSON.stringify(integrityDocument, null, 2)}\n`, + ); + + console.log( + `\n✓ Staged and verified ${order.length} package(s) under .publish/ (access: ${ACCESS})`, + ); console.log("Publish order (deps first):"); for (const n of order) { console.log(` npm publish .publish/${n.replace("@wrnexus/", "")} --access ${ACCESS}`); diff --git a/scripts/release.ts b/scripts/release.ts index e1b8fa14..51f74a1a 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -13,6 +13,7 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; import { updateMigrationVersions } from "../packages/cli/src/update.ts"; +import { validateAndHashStage } from "./lib/package-integrity.ts"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const docsRoot = resolve(process.env.WRNEXUS_DOCS_ROOT ?? "D:\\Company\\wrnexusjs"); @@ -106,6 +107,18 @@ function requireMigration(version: string) { } function validateStaging(all: PackageInfo[], version: string) { + const integrityPath = join(root, ".publish", "PACKAGE-INTEGRITY.json"); + if (!existsSync(integrityPath)) { + throw new Error("Missing .publish/PACKAGE-INTEGRITY.json; stage verification did not run."); + } + const integrity = JSON.parse(readFileSync(integrityPath, "utf8")); + const recorded = new Map( + (Array.isArray(integrity.packages) ? integrity.packages : []).map((entry: any) => [ + entry.name, + entry, + ]), + ); + for (const pkg of all) { const stage = join(root, ".publish", pkg.dir); const manifestPath = join(stage, "package.json"); @@ -117,6 +130,18 @@ function validateStaging(all: PackageInfo[], version: string) { if (manifest.version !== version || manifest.publishConfig?.access !== "restricted") { throw new Error(`${pkg.name} staging is not restricted ${version}.`); } + + const actual = validateAndHashStage(stage, manifest); + const expected = recorded.get(pkg.name); + if (!expected || JSON.stringify(expected) !== JSON.stringify(actual)) { + throw new Error(`${pkg.name} staging integrity does not match PACKAGE-INTEGRITY.json.`); + } + } + + if (recorded.size !== all.length) { + throw new Error( + `Staging integrity lists ${recorded.size} packages but the workspace contains ${all.length}.`, + ); } } @@ -199,6 +224,11 @@ function prepare(all: PackageInfo[], version: string) { run(process.execPath, ["run", "scripts/generate-ui-component-reference.mjs"], root); run(process.execPath, ["run", "format"]); + run(process.execPath, ["run", "check:workspace"]); + run(process.execPath, ["run", "validate:0.7"]); + run(process.execPath, ["run", "security:framework"]); + run(process.execPath, ["run", "sbom"]); + run(process.execPath, ["run", "benchmark:framework"]); run(process.execPath, ["run", "check"]); run(process.execPath, ["run", "scripts/publish-packages.ts"]); validateStaging(all, version); @@ -230,11 +260,17 @@ function verifyUiComponentReference(): void { throw new Error(`Failed to generate the UI component reference.${detail ? `\n${detail}` : ""}`); } - // Format generated files before comparing them with Git. - // The generator expands JSON arrays, while Prettier may place short arrays - // on one line. Comparing before formatting creates a permanent false diff. + // The generator intentionally produces stable semantic JSON, while Prettier + // controls repository formatting. Format only the generated files before the + // Git comparison so short arrays and Windows line endings cannot create a + // permanent generate/format release loop. run(process.execPath, ["x", "prettier", "--write", ...generatedFiles], root); + // Do not use `git status --short` here. On Windows, the generator writes LF + // while an autocrlf checkout may contain CRLF. `git status` can report those + // files as modified even when their canonical Git content is identical. + // `git diff --quiet` applies Git's normal text conversion and only fails for + // a real generated-content difference. const changedStatus = run("git", ["diff", "--quiet", "--", ...generatedFiles], root, { capture: true, allowFailure: true, @@ -254,9 +290,9 @@ function verifyUiComponentReference(): void { "", "Run:", " bun run scripts/generate-ui-component-reference.mjs", - " bunx prettier --write packages/ui/component-catalog.json packages/ui/component-reference.json packages/ui/COMPONENTS.md", + " bun run format", " git add packages/ui/component-catalog.json packages/ui/component-reference.json packages/ui/COMPONENTS.md", - ' git commit -m \\"docs(ui): refresh component reference\\"', + ' git commit -m "docs(ui): refresh component reference"', " git push origin main", ].join("\n"), ); @@ -265,6 +301,9 @@ function verifyUiComponentReference(): void { function publish(all: PackageInfo[], version: string) { if (!existsSync(docsRoot)) throw new Error(`Docs repository not found: ${docsRoot}`); requireMigration(version); + run(process.execPath, ["run", "check:workspace"]); + run(process.execPath, ["run", "validate:0.7"]); + run(process.execPath, ["run", "security:framework"]); requireCleanAndPushed(root, "Framework"); requireCleanAndPushed(docsRoot, "Docs"); diff --git a/scripts/repair-workspace.mjs b/scripts/repair-workspace.mjs new file mode 100644 index 00000000..de68cc87 --- /dev/null +++ b/scripts/repair-workspace.mjs @@ -0,0 +1,99 @@ +import console from "node:console"; +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import process from "node:process"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const dryRun = process.argv.includes("--check"); +const removed = []; +const untracked = []; +const warnings = []; + +function removeLocal(path) { + const absolute = join(root, path); + if (!existsSync(absolute)) return; + if (!dryRun) unlinkSync(absolute); + removed.push(path); +} + +function git(args, capture = false) { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: capture ? ["ignore", "pipe", "pipe"] : ["ignore", "ignore", "pipe"], + }); +} + +function isGitRepository() { + try { + return git(["rev-parse", "--is-inside-work-tree"], true).trim() === "true"; + } catch { + return false; + } +} + +function trackedFiles() { + try { + return git(["ls-files", "-z"], true).split("\0").filter(Boolean); + } catch { + return []; + } +} + +function ensureIgnoreRules() { + const path = join(root, ".gitignore"); + const current = existsSync(path) ? readFileSync(path, "utf8") : ""; + const required = [ + ".env", + ".env.*", + "!.env.example", + "!.env.*.example", + "focus-shims.d.ts", + "tsconfig.focus.json", + ]; + const lines = new Set(current.split(/\r?\n/).map((line) => line.trim())); + const missing = required.filter((line) => !lines.has(line)); + if (missing.length === 0) return; + if (!dryRun) { + const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : ""; + writeFileSync(path, `${current}${separator}${missing.join("\n")}\n`); + } + warnings.push(`Added missing .gitignore rules: ${missing.join(", ")}`); +} + +removeLocal("focus-shims.d.ts"); +removeLocal("tsconfig.focus.json"); +ensureIgnoreRules(); + +if (isGitRepository()) { + const secretPattern = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|.*\.(?:pem|p12|pfx|key))$/i; + const safeTemplate = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i; + const temporaryPattern = /(?:^|\/)(?:focus-shims\.d\.ts|tsconfig\.focus\.json)$/i; + const unsafeTracked = trackedFiles().filter( + (path) => temporaryPattern.test(path) || (secretPattern.test(path) && !safeTemplate.test(path)), + ); + for (const path of unsafeTracked) { + if (!dryRun) git(["rm", "--cached", "--ignore-unmatch", "--", path]); + untracked.push(path); + } +} else { + warnings.push( + "Git metadata was not found, so tracked secret files could not be removed from the Git index.", + ); +} + +console.log(dryRun ? "Workspace repair check:" : "Workspace repaired:"); +console.log( + `- ${dryRun ? "would remove" : "removed"} temporary files: ${removed.length ? removed.join(", ") : "none"}`, +); +console.log( + `- ${dryRun ? "would remove" : "removed"} from Git tracking: ${untracked.length ? untracked.join(", ") : "none"}`, +); +for (const warning of warnings) console.warn(`- warning: ${warning}`); + +if (dryRun && (removed.length > 0 || untracked.length > 0)) { + console.error("Workspace repair is required. Run: bun run repair:workspace"); + process.exitCode = 1; +} diff --git a/scripts/security-performance-audit.mjs b/scripts/security-performance-audit.mjs new file mode 100644 index 00000000..b8f6e6a1 --- /dev/null +++ b/scripts/security-performance-audit.mjs @@ -0,0 +1,204 @@ +import console from "node:console"; +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs"; +import { dirname, extname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import process from "node:process"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const issues = []; +const warnings = []; +const checks = []; + +function addCheck(name, passed, detail) { + checks.push({ name, passed, detail }); + if (!passed) issues.push({ severity: "error", code: name, detail }); +} + +function addWarning(name, detail) { + warnings.push({ severity: "warning", code: name, detail }); +} + +function walk(dir, output = []) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (["node_modules", ".git", ".publish", "dist", ".wrnexus"].includes(entry.name)) continue; + const path = join(dir, entry.name); + if (entry.isDirectory()) walk(path, output); + else output.push(path); + } + return output; +} + +function listGitTrackedFiles() { + try { + const output = execFileSync("git", ["ls-files", "-z"], { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + return output + .split("\0") + .filter(Boolean) + .map((path) => join(root, path)) + .filter((path) => existsSync(path) && statSync(path).isFile()); + } catch { + // Source archives do not contain .git metadata. In that environment the + // archive itself is the release boundary, so scanning every included file + // is the correct fallback. + return walk(root); + } +} + +const allFiles = walk(root); +const trackedFiles = listGitTrackedFiles(); +const trackedSet = new Set(trackedFiles.map((path) => resolve(path))); + +const manifests = trackedFiles + .filter((path) => path.startsWith(join(root, "packages")) && path.endsWith("package.json")) + .map((path) => ({ path, value: JSON.parse(readFileSync(path, "utf8")) })) + .filter(({ value }) => value.name?.startsWith("@wrnexus/")); +addCheck( + "SEC-PACKAGE-VERSION", + manifests.every(({ value }) => value.version === "0.7.0"), + "Every @wrnexus package must use version 0.7.0.", +); +addCheck( + "SEC-PRIVATE-PUBLISH", + manifests.every(({ value }) => value.publishConfig?.access !== "public"), + "No framework package may opt into public access in a private release.", +); + +const secretNames = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|.*\.(?:pem|p12|pfx|key))$/i; +const safeSecretTemplates = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i; +function isSecretLike(path) { + const normalized = relative(root, path).replace(/\\/g, "/"); + return secretNames.test(normalized) && !safeSecretTemplates.test(normalized); +} + +const trackedSecretFiles = trackedFiles.filter(isSecretLike); +addCheck( + "SEC-NO-TRACKED-SECRET-FILES", + trackedSecretFiles.length === 0, + trackedSecretFiles.length > 0 + ? `${trackedSecretFiles.map((path) => relative(root, path)).join(", ")}; run: bun run repair:workspace` + : "No tracked secret-like files found.", +); + +const temporaryTypecheckPattern = /(?:^|\/)(?:focus-shims\.d\.ts|tsconfig\.focus\.json)$/i; +const trackedTypecheckHelpers = trackedFiles.filter((path) => + temporaryTypecheckPattern.test(relative(root, path).replace(/\\/g, "/")), +); +addCheck( + "SEC-NO-TRACKED-TYPECHECK-SHIMS", + trackedTypecheckHelpers.length === 0, + trackedTypecheckHelpers.length > 0 + ? `${trackedTypecheckHelpers.map((path) => relative(root, path)).join(", ")}; run: bun run repair:workspace` + : "No temporary typecheck helpers are tracked.", +); + +const localTypecheckHelpers = allFiles.filter( + (path) => + temporaryTypecheckPattern.test(relative(root, path).replace(/\\/g, "/")) && + !trackedSet.has(resolve(path)), +); +if (localTypecheckHelpers.length > 0) { + addWarning( + "SEC-LOCAL-TYPECHECK-SHIMS", + `Temporary local typecheck helpers are ignored by the root TypeScript program and can be removed with bun run repair:workspace: ${localTypecheckHelpers + .map((path) => relative(root, path)) + .join(", ")}`, + ); +} + +const localSecretFiles = allFiles.filter( + (path) => isSecretLike(path) && !trackedSet.has(resolve(path)), +); +if (localSecretFiles.length > 0) { + addWarning( + "SEC-LOCAL-SECRET-FILES", + `Ignored or untracked local secret files were not included in the release audit: ${localSecretFiles + .map((path) => relative(root, path)) + .join(", ")}`, + ); +} + +const wrnFiles = trackedFiles.filter((path) => extname(path) === ".wrn"); +const dangerousUrls = []; +for (const path of wrnFiles) { + const source = readFileSync(path, "utf8"); + if ( + /\b(?:href|src|action|formaction)\s*=\s*["']\s*(?:javascript:|vbscript:|file:)/i.test(source) + ) { + dangerousUrls.push(relative(root, path)); + } +} +addCheck( + "SEC-NO-DANGEROUS-STATIC-URLS", + dangerousUrls.length === 0, + dangerousUrls.join(", ") || "No dangerous static URLs found.", +); + +const productionFiles = trackedFiles.filter((path) => + /packages\/(?:security|cache|image|observability|ssr|core)\/src\/.+\.ts$/.test( + path.replace(/\\/g, "/"), + ), +); +const dynamicCode = []; +for (const path of productionFiles) { + const source = readFileSync(path, "utf8"); + if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(source)) dynamicCode.push(relative(root, path)); +} +addCheck( + "SEC-FOUNDATION-NO-DYNAMIC-CODE", + dynamicCode.length === 0, + dynamicCode.join(", ") || "No dynamic code execution in security foundation packages.", +); + +const runtimePath = join(root, "packages", "csr", "src", "reactive-runtime.ts"); +addCheck( + "PERF-RUNTIME-SIZE", + statSync(runtimePath).size < 250_000, + `Reactive runtime source is ${statSync(runtimePath).size} bytes.`, +); +addCheck( + "PERF-ZERO-JS-ANALYSIS", + existsSync(join(root, "packages", "compiler", "src", "analysis.ts")), + "Static/interactive route analysis must exist.", +); +addCheck( + "PERF-BROTLI", + readFileSync(join(root, "packages", "dev-server", "src", "runtime.ts"), "utf8").includes( + "brotliCompressSync", + ), + "Production runtime should prefer Brotli.", +); +addCheck( + "OBS-METRICS", + existsSync(join(root, "packages", "observability", "src", "metrics.ts")), + "Metrics registry must exist.", +); +addCheck( + "SUPPLY-SBOM", + existsSync(join(root, "scripts", "generate-sbom.mjs")), + "SBOM generator must exist.", +); + +const report = { + schemaVersion: 2, + frameworkVersion: "0.7.0", + generatedAt: new Date().toISOString(), + passed: issues.length === 0, + checks, + warnings, + issues, +}; +const reportDir = join(root, ".wrnexus", "reports"); +mkdirSync(reportDir, { recursive: true }); +writeFileSync( + join(reportDir, "security-performance-0.7.0.json"), + JSON.stringify(report, null, 2) + "\n", +); +for (const check of checks) + console.log(`${check.passed ? "ok" : "FAIL"} ${check.name} — ${check.detail}`); +for (const warning of warnings) console.warn(`warn ${warning.code} — ${warning.detail}`); +if (issues.length) process.exitCode = 1; diff --git a/scripts/test-package-integrity.mjs b/scripts/test-package-integrity.mjs new file mode 100644 index 00000000..45088316 --- /dev/null +++ b/scripts/test-package-integrity.mjs @@ -0,0 +1,66 @@ +import console from "node:console"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { validateAndHashStage } from "./lib/package-integrity.ts"; + +const root = mkdtempSync(join(tmpdir(), "wrnexus-stage-integrity-")); +const manifest = { name: "@wrnexus/probe", version: "0.7.0" }; +try { + const safe = join(root, "safe"); + mkdirSync(join(safe, "dist"), { recursive: true }); + writeFileSync(join(safe, "dist", "index.js"), "export const safe = true;\n"); + writeFileSync(join(safe, "dist", "index.d.ts"), "export declare const safe: true;\n"); + const result = validateAndHashStage(safe, manifest); + if (result.files.length !== 2 || result.files.some((file) => file.sha256.length !== 64)) { + throw new Error("Safe stage did not produce deterministic SHA-256 records."); + } + + const forbidden = join(root, "forbidden"); + mkdirSync(forbidden); + writeFileSync(join(forbidden, ".env"), "TOKEN=secret\n"); + let rejected = false; + try { + validateAndHashStage(forbidden, manifest); + } catch { + rejected = true; + } + if (!rejected) throw new Error("Secret-like stage file was not rejected."); + + const temporaryShim = join(root, "temporary-shim"); + mkdirSync(temporaryShim); + writeFileSync(join(temporaryShim, "focus-shims.d.ts"), "declare const Bun: any;\n"); + rejected = false; + try { + validateAndHashStage(temporaryShim, manifest); + } catch { + rejected = true; + } + if (!rejected) throw new Error("Temporary typecheck shim was not rejected."); + + const sourceMap = join(root, "source-map"); + mkdirSync(sourceMap); + writeFileSync(join(sourceMap, "index.js.map"), "{}\n"); + rejected = false; + try { + validateAndHashStage(sourceMap, manifest); + } catch { + rejected = true; + } + if (!rejected) throw new Error("Source map was not rejected."); + + const leaked = join(root, "leaked-secret"); + mkdirSync(leaked); + writeFileSync(join(leaked, "index.js"), 'export const key = "-----BEGIN PRIVATE KEY-----";\n'); + rejected = false; + try { + validateAndHashStage(leaked, manifest); + } catch { + rejected = true; + } + if (!rejected) throw new Error("High-confidence secret content was not rejected."); + + console.log("Package staging integrity probes passed."); +} finally { + rmSync(root, { recursive: true, force: true }); +} diff --git a/scripts/test-workspace-repair.mjs b/scripts/test-workspace-repair.mjs new file mode 100644 index 00000000..ca229217 --- /dev/null +++ b/scripts/test-workspace-repair.mjs @@ -0,0 +1,91 @@ +import console from "node:console"; +import { execFileSync } from "node:child_process"; +import process from "node:process"; +import { + cpSync, + existsSync, + mkdtempSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const frameworkRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const root = mkdtempSync(join(tmpdir(), "wrnexus-workspace-repair-")); + +function git(args) { + return execFileSync("git", args, { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }); +} + +try { + mkdirSync(join(root, "scripts"), { recursive: true }); + mkdirSync(join(root, "examples", "basic-app"), { recursive: true }); + cpSync( + join(frameworkRoot, "scripts", "repair-workspace.mjs"), + join(root, "scripts", "repair-workspace.mjs"), + ); + writeFileSync( + join(root, ".gitignore"), + [".env", ".env.*", "!.env.example", "!.env.*.example", ""].join("\n"), + ); + writeFileSync(join(root, "examples", "basic-app", ".env.example"), "TOKEN=replace-me\n"); + writeFileSync(join(root, "examples", "basic-app", ".env.uat.example"), "TOKEN=replace-me\n"); + + git(["init", "-q"]); + git(["config", "user.email", "test@wrnexus.invalid"]); + git(["config", "user.name", "WRNexus Test"]); + git(["add", "-A"]); + git(["commit", "-qm", "baseline"]); + + writeFileSync(join(root, "examples", "basic-app", ".env"), "TOKEN=keep-local\n"); + writeFileSync(join(root, "examples", "basic-app", ".env.uat"), "TOKEN=keep-local-uat\n"); + writeFileSync(join(root, "focus-shims.d.ts"), "declare const Bun: any;\n"); + writeFileSync(join(root, "tsconfig.focus.json"), "{}\n"); + git([ + "add", + "-f", + "examples/basic-app/.env", + "examples/basic-app/.env.uat", + "focus-shims.d.ts", + "tsconfig.focus.json", + ]); + git(["commit", "-qm", "unsafe local state"]); + + execFileSync(process.execPath, [join(root, "scripts", "repair-workspace.mjs")], { + cwd: root, + stdio: "pipe", + }); + + if (readFileSync(join(root, "examples", "basic-app", ".env"), "utf8") !== "TOKEN=keep-local\n") { + throw new Error("Local .env value was modified or removed."); + } + if ( + readFileSync(join(root, "examples", "basic-app", ".env.uat"), "utf8") !== + "TOKEN=keep-local-uat\n" + ) { + throw new Error("Local .env.uat value was modified or removed."); + } + if (existsSync(join(root, "focus-shims.d.ts")) || existsSync(join(root, "tsconfig.focus.json"))) { + throw new Error("Temporary typecheck files were not removed."); + } + + const tracked = git(["ls-files", "-z"]).split("\0").filter(Boolean); + const secretPattern = /(?:^|\/)(?:\.env(?:\..+)?|focus-shims\.d\.ts|tsconfig\.focus\.json)$/i; + const safeTemplate = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i; + const forbidden = tracked.filter((path) => secretPattern.test(path) && !safeTemplate.test(path)); + if (forbidden.length > 0) { + throw new Error(`Unsafe files remain tracked: ${forbidden.join(", ")}`); + } + + console.log("Workspace repair regression test passed."); +} finally { + rmSync(root, { recursive: true, force: true }); +} diff --git a/scripts/validate-0.6.mjs b/scripts/validate-0.6.mjs index 9fb95995..dca5c784 100644 --- a/scripts/validate-0.6.mjs +++ b/scripts/validate-0.6.mjs @@ -32,16 +32,20 @@ function walk(dir, predicate = () => true) { const packageDirs = readdirSync(join(root, "packages")) .filter((name) => existsSync(join(root, "packages", name, "package.json"))) .sort(); -if (packageDirs.length !== 33) fail(`Expected 33 framework packages, found ${packageDirs.length}`); -else pass("33 framework packages are present"); +const frameworkVersion = readJson(join(root, "package.json")).version; +if (packageDirs.length < 33) + fail(`Expected at least 33 framework packages, found ${packageDirs.length}`); +else pass(`${packageDirs.length} framework packages are present`); for (const name of packageDirs) { const manifest = readJson(join(root, "packages", name, "package.json")); - if (manifest.version !== "0.6.0") - fail(`packages/${name} is ${manifest.version ?? "unversioned"}`); + if (manifest.version !== frameworkVersion) { + fail(`packages/${name} is ${manifest.version ?? "unversioned"}; expected ${frameworkVersion}`); + } +} +if (!failures.some((item) => item.startsWith("packages/"))) { + pass(`All framework packages are version ${frameworkVersion}`); } -if (!failures.some((item) => item.startsWith("packages/"))) - pass("All framework packages are version 0.6.0"); for (const [label, path] of [ ["root", "package.json"], @@ -49,8 +53,9 @@ for (const [label, path] of [ ["managed CAPTCHA service", "services/managed-captcha/package.json"], ]) { const version = readJson(join(root, path)).version; - if (version !== "0.6.0") fail(`${label} version is ${version}`); - else pass(`${label} version is 0.6.0`); + if (version !== frameworkVersion) + fail(`${label} version is ${version}; expected ${frameworkVersion}`); + else pass(`${label} version is ${frameworkVersion}`); } for (const path of [ diff --git a/scripts/validate-0.7.mjs b/scripts/validate-0.7.mjs new file mode 100644 index 00000000..3c5bb70b --- /dev/null +++ b/scripts/validate-0.7.mjs @@ -0,0 +1,245 @@ +import console from "node:console"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import process from "node:process"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +let passed = 0; +let failed = 0; +let warned = 0; +const failures = []; + +function check(name, condition, detail = "") { + if (condition) { + passed += 1; + console.log(` ok ${name}`); + return; + } + failed += 1; + failures.push(detail ? `${name}: ${detail}` : name); + console.error(` FAIL ${name}${detail ? ` — ${detail}` : ""}`); +} + +function warn(name, detail = "") { + warned += 1; + console.warn(` warn ${name}${detail ? ` — ${detail}` : ""}`); +} + +function text(path) { + return readFileSync(join(root, path), "utf8"); +} + +function has(path, pattern) { + const value = text(path); + return typeof pattern === "string" ? value.includes(pattern) : pattern.test(value); +} + +const packageDirs = readdirSync(join(root, "packages"), { withFileTypes: true }) + .filter( + (entry) => + entry.isDirectory() && existsSync(join(root, "packages", entry.name, "package.json")), + ) + .map((entry) => entry.name); +const manifests = packageDirs + .map((dir) => ({ + dir, + manifest: JSON.parse(text(`packages/${dir}/package.json`)), + })) + .filter(({ manifest }) => manifest.name?.startsWith("@wrnexus/")); + +check("root version is 0.7.0", JSON.parse(text("package.json")).version === "0.7.0"); +const temporaryTypecheckFiles = ["focus-shims.d.ts", "tsconfig.focus.json"].filter((path) => + existsSync(join(root, path)), +); +const rootTsconfig = JSON.parse(text("tsconfig.json")); +const rootExcludes = new Set(rootTsconfig.exclude ?? []); +check( + "root typecheck excludes temporary focused files", + rootExcludes.has("focus-shims.d.ts") && + rootExcludes.has("tsconfig.focus.json") && + rootExcludes.has("**/focus-shims.d.ts") && + rootExcludes.has("**/tsconfig.focus.json"), + "Keep the temporary focused typecheck files outside the root TypeScript program.", +); +if (temporaryTypecheckFiles.length > 0) { + warn( + "temporary focused typecheck files are local leftovers", + `${temporaryTypecheckFiles.join(", ")} will be removed by: bun run repair:workspace`, + ); +} +check( + "lint and formatting ignore temporary focused files", + has("eslint.config.js", '"focus-shims.d.ts"') && + has("eslint.config.js", '"tsconfig.focus.json"') && + has(".prettierignore", "focus-shims.d.ts") && + has(".prettierignore", "tsconfig.focus.json"), + "Temporary focused helpers must not create lint or formatting cascades before workspace repair.", +); +check( + "all framework package versions align", + manifests.every(({ manifest }) => manifest.version === "0.7.0"), + [...new Set(manifests.map(({ manifest }) => manifest.version))].join(", "), +); +for (const name of ["security", "cache", "image", "observability", "benchmark"]) { + check( + `@wrnexus/${name} package exists`, + existsSync(join(root, "packages", name, "src", "index.ts")), + ); +} + +check( + "context-aware URL sanitizer is wired into compiler", + has("packages/compiler/src/codegen.ts", "sanitizeUrlAttribute"), +); +check( + "URL security checks avoid lint-blocked control-character regexes", + has("packages/compiler/src/codegen.ts", "stripAsciiControlAndSpace") && + has("packages/security/src/url.ts", "hasAsciiControlOrSpace") && + has("packages/syntax/src/diagnostics.ts", "stripAsciiControlAndSpace") && + !has("packages/compiler/src/codegen.ts", /\\u0000-\\u0020/) && + !has("packages/security/src/url.ts", /\\u0000-\\u0020/) && + !has("packages/syntax/src/diagnostics.ts", /\\u0000-\\u0020/), + "Use character-code checks instead of control-character regular expressions.", +); +check( + "trusted HTML requires an explicit policy", + has("packages/security/src/trusted-html.ts", "createTrustedHtml"), +); +check( + "reactive URL updates are sanitized", + has("packages/csr/src/reactive-runtime.ts", "sanitizeReactiveUrl"), +); +check( + "SSR store state uses secure serialization", + has("packages/ssr/src/store-context.ts", "serializeForHtml"), +); +check("prototype pollution keys are rejected", has("packages/security/src/object.ts", "__proto__")); +check( + "SSRF helper blocks private networks", + has("packages/security/src/fetch.ts", "isPrivateAddress"), +); +check( + "CSRF checks Origin and Fetch Metadata", + has("packages/core/src/csrf.ts", /sec-fetch-site|validateOrigin/), +); +check( + "security headers include CSP and isolation", + has("packages/core/src/headers.ts", "Origin-Agent-Cluster"), +); +check( + "request limits are automatic runtime middleware", + has("packages/dev-server/src/runtime.ts", "requestHardening"), +); +check( + "gateway enforces request limits", + has("packages/dev-server/src/gateway.ts", "GatewayRequestLimits"), +); +check( + "gateway enforces WebSocket limits", + has("packages/dev-server/src/gateway.ts", "maxQueuedMessages"), +); +check( + "realtime enforces per-room security", + has("packages/core/src/realtime.ts", "RealtimeSecurityOptions"), +); +check( + "database instrumentation detects N+1 patterns", + has("packages/db/src/performance.ts", "WRN-DB-DUPLICATE-QUERY"), +); +check( + "CSS performance audit is available", + has("packages/styles/src/audit.ts", "WRN-CSS-TRANSITION-ALL"), +); +check( + "static runtime analysis is exported", + has("packages/compiler/src/analysis.ts", "analyzeRuntimeRequirements"), +); +check( + "static pages default to zero framework JavaScript", + has("packages/dev-server/src/runtime.ts", 'navigation.mode ?? "auto"') && + has("packages/dev-server/src/runtime.ts", "scripts.length > 0"), +); +check( + "Brotli delivery is preferred", + has("packages/dev-server/src/runtime.ts", "brotliCompressSync"), +); +check( + "Happy DOM validation events and elements use one DOM type system", + has("packages/validation/test/validation.test.ts", "type Event as HappyDOMEvent") && + has( + "packages/validation/test/validation.test.ts", + "type HTMLInputElement as HappyDOMHTMLInputElement", + ) && + has( + "packages/validation/test/validation.test.ts", + "type HTMLFormElement as HappyDOMHTMLFormElement", + ) && + has( + "packages/validation/test/validation.test.ts", + "function windowEvent(win: Window, type: string, init?: IEventInit): HappyDOMEvent", + ) && + !has( + "packages/validation/test/validation.test.ts", + /as unknown as (?:HTMLInputElement|HTMLFormElement|HTMLElement)/, + ), + "Use Happy DOM Event and element types together; do not mix them with browser DOM globals.", +); +check( + "DevToolbar security checks include CSRF", + has("packages/dev-toolbar/src/rules/security.ts", "security/missing-csrf"), +); +check( + "DevToolbar performance checks include hydration", + has("packages/dev-toolbar/src/rules/performance.ts", "performance/hydration-count"), +); +check("release migration includes 0.7.0", has("packages/cli/src/update.ts", 'version: "0.7.0"')); +check( + "21-point implementation matrix exists", + existsSync(join(root, "docs", "SECURITY-PERFORMANCE-0.7.md")), +); +check( + "production audit script exists", + existsSync(join(root, "scripts", "security-performance-audit.mjs")), +); +check( + "workspace repair command exists", + existsSync(join(root, "scripts", "repair-workspace.mjs")) && + existsSync(join(root, "scripts", "test-workspace-repair.mjs")) && + JSON.parse(text("package.json")).scripts?.["repair:workspace"] === + "node scripts/repair-workspace.mjs" && + JSON.parse(text("package.json")).scripts?.["check:workspace"] === + "node scripts/repair-workspace.mjs --check" && + has("scripts/release.ts", "check:workspace"), +); +check( + "security audit scans Git-tracked release files", + has("scripts/security-performance-audit.mjs", 'execFileSync("git", ["ls-files", "-z"]') && + has("scripts/security-performance-audit.mjs", "SEC-NO-TRACKED-SECRET-FILES"), +); +check("SBOM generator exists", existsSync(join(root, "scripts", "generate-sbom.mjs"))); +check( + "SBOM includes transitive bun.lock packages", + has("scripts/generate-sbom.mjs", "lock.packages"), +); +check( + "SBOM parse errors preserve their original cause", + has("scripts/generate-sbom.mjs", "{ cause: error }") && + has("scripts/test-workspace-repair.mjs", 'import process from "node:process"'), +); +check( + "staged packages are secret-scanned and hashed", + has("scripts/publish-packages.ts", "validateAndHashStage") && + has("scripts/publish-packages.ts", "PACKAGE-INTEGRITY.json"), +); +check("benchmark runner exists", existsSync(join(root, "scripts", "benchmark-framework.mjs"))); +check("release gate formats generated UI references", has("scripts/release.ts", "--write")); + +console.log(`\n${passed} passed`); +console.log(`${warned} warnings`); +console.log(`${failed} failed`); +if (failed) { + console.error("\nFailures:"); + for (const failure of failures) console.error(`- ${failure}`); + process.exitCode = 1; +} diff --git a/services/managed-captcha/package.json b/services/managed-captcha/package.json index 79158c61..9064f8eb 100644 --- a/services/managed-captcha/package.json +++ b/services/managed-captcha/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/managed-captcha-service", - "version": "0.6.0", + "version": "0.7.0", "private": true, "type": "module", "scripts": { diff --git a/tsconfig.json b/tsconfig.json index c4b2b021..9ec10e1a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -114,8 +114,28 @@ "@wrnexus/typecheck": ["./packages/typecheck/src/index.ts"], "@wrnexus/typecheck/*": ["./packages/typecheck/src/*.ts"], "@wrnexus/ssr/rpc": ["./packages/ssr/src/rpc.ts"], - "@wrnexus/ssr/store-context": ["./packages/ssr/src/store-context.ts"] + "@wrnexus/ssr/store-context": ["./packages/ssr/src/store-context.ts"], + "@wrnexus/security": ["./packages/security/src/index.ts"], + "@wrnexus/security/*": ["./packages/security/src/*.ts"], + "@wrnexus/cache": ["./packages/cache/src/index.ts"], + "@wrnexus/cache/*": ["./packages/cache/src/*.ts"], + "@wrnexus/image": ["./packages/image/src/index.ts"], + "@wrnexus/image/*": ["./packages/image/src/*.ts"], + "@wrnexus/observability": ["./packages/observability/src/index.ts"], + "@wrnexus/observability/*": ["./packages/observability/src/*.ts"], + "@wrnexus/benchmark": ["./packages/benchmark/src/index.ts"], + "@wrnexus/benchmark/*": ["./packages/benchmark/src/*.ts"] } }, - "exclude": ["node_modules", "dist", "**/dist", "**/.wrnexus"] + "exclude": [ + "node_modules", + "dist", + "**/dist", + "**/.wrnexus", + "focus-shims.d.ts", + "tsconfig.focus.json", + "**/focus-shims.d.ts", + "**/tsconfig.focus.json" + ], + "include": ["**/*.ts", "**/*.mts", "**/*.cts"] } diff --git a/update-package-versions.mjs b/update-package-versions.mjs index ba521645..f4cbddf4 100644 --- a/update-package-versions.mjs +++ b/update-package-versions.mjs @@ -1,7 +1,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -const version = "0.6.0"; +const version = "0.7.0"; const dependencyGroups = [ "dependencies",