Compare commits

...
67 Commits
Author SHA1 Message Date
Clintchiz 84e16ebc41 chore(release): publish ui 0.8.56
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:36:04 +05:30
Clintchiz 7993a303ac fix(ui): preserve viewport anchoring for centered navbar
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:35:43 +05:30
Clintchiz a99290a216 chore(release): publish ui 0.8.55
Quality / quality (ubuntu-latest) (push) Failing after 10m16s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:34:17 +05:30
Clintchiz c648efc240 fix(ui): contain long mega menu labels
Quality / quality (ubuntu-latest) (push) Failing after 10m41s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:33:59 +05:30
Clintchiz decbee2fd1 chore(release): publish ui 0.8.54
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:31:38 +05:30
Clintchiz e62e7f5d27 fix(ui): center desktop navbar menus
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 19:31:17 +05:30
Clintchiz b2c74a3a29 chore(release): publish ui 0.8.53
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:59:43 +05:30
Clintchiz fe86570f45 fix(ui): render dynamic tab panels and ids
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:59:10 +05:30
Clintchiz bc1211ea8b chore(release): publish ui 0.8.52
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:37:55 +05:30
Clintchiz d2e24eb089 feat(ui): refine timeline and blockquote layouts
Quality / quality (ubuntu-latest) (push) Failing after 10m2s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:37:29 +05:30
Clintchiz e71885d51c chore(release): publish ui 0.8.51
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:31:26 +05:30
Clintchiz 138b5d0950 fix(ui): prevent full bleed hero overflow
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 17:30:34 +05:30
Clintchiz 7e1eb899b3 fix(ui): align stats surface within max width
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:54:23 +05:30
Clintchiz 3f6612cfe7 fix(ui): honor stats bar desktop gutters
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:50:10 +05:30
Clintchiz 510b3ba777 fix(ui): keep breadcrumb separators visible
Quality / quality (ubuntu-latest) (push) Failing after 10m21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:34:44 +05:30
Clintchiz 1ff2c3339c fix(ui): render active breadcrumb pages
Quality / quality (ubuntu-latest) (push) Failing after 10m0s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 16:21:53 +05:30
Clintchiz 027873459d fix(ui): collapse empty page header slots
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:56:57 +05:30
Clintchiz d0bccfe37e fix(csr): suppress inactive theme token warnings
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:48:48 +05:30
Clintchiz 120889060a fix(ui): normalize form field label sizing
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:25:24 +05:30
Clintchiz 26ec18c24b chore(editor): release vscode extension 0.8.11
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:22:15 +05:30
Clintchiz 7658c5d499 fix(editor): type nested attribute expressions
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 15:16:38 +05:30
Clintchiz 3cb6ba1233 fix(ui): render navbar brand prop outside slot
Quality / quality (ubuntu-latest) (push) Failing after 10m32s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 14:00:46 +05:30
Clintchiz 64db7da08d fix(ui): refine responsive catalog navigation
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-25 13:46:19 +05:30
Clintchiz 9f6552ea62 fix(ui): separate catalog icon masks from tiles
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:30:33 +05:30
Clintchiz 5bc391e74f fix(ui): inherit catalog icon contrast
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:23:43 +05:30
Clintchiz 3327ec0d37 refactor(ui): clarify catalog navigation hierarchy
Quality / quality (ubuntu-latest) (push) Failing after 10m18s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:16:22 +05:30
Clintchiz ceb865f4f1 fix(ui): prevent navigation surface overflow
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:08:00 +05:30
Clintchiz 45309b3f72 refactor(ui): compact mega menu layouts
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 23:06:01 +05:30
Clintchiz 8f3a13c854 fix(ui): render only supplied mega menu metadata
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:57:13 +05:30
Clintchiz eca4447dcf fix(ui): stabilize catalog mega menu interaction
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:50:41 +05:30
Clintchiz 904bca21b2 refactor(ui): refine catalog mega menu navigation
Quality / quality (ubuntu-latest) (push) Failing after 10m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:40:58 +05:30
Clintchiz b98d417185 fix(ui): eliminate mega menu flash and lag
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:33:02 +05:30
Clintchiz 7d737ec0a0 fix(ui): make catalog mega menus interactive
Quality / quality (ubuntu-latest) (push) Failing after 6m15s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:18:30 +05:30
Clintchiz f5538ed621 fix(ui): keep mega menu open across hover gap
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 22:08:46 +05:30
Clintchiz d2d830e7e2 fix(ui): pin navbar mega panels to viewport
Quality / quality (ubuntu-latest) (push) Failing after 12m5s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:56:27 +05:30
Clintchiz 0618782355 fix(ui): make mega menus work under strict CSP
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:52:25 +05:30
Clintchiz 9067e68f69 fix(ui): import navbar mega menu dependency
Quality / quality (ubuntu-latest) (push) Failing after 10m39s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:37:26 +05:30
Clintchiz 9d64ec60c4 feat(ui): add typed navbar menu items
Quality / quality (ubuntu-latest) (push) Failing after 11m12s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:36:46 +05:30
Clintchiz fc1eea3939 chore(editor): refresh language server bundle hash
Quality / quality (ubuntu-latest) (push) Failing after 10m8s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:25:53 +05:30
Clintchiz 4ec5dad60a fix(formatter): expand large structured prop defaults
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:25:07 +05:30
Clintchiz a1a2947910 feat(ui): expand mega menu layout system
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 21:15:50 +05:30
Clintchiz 270040fb5f feat(ui): add in-flow floating navbar
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 19:47:47 +05:30
Clintchiz f9e285b182 feat(ui): own responsive marketing layout APIs
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 19:44:26 +05:30
Clintchiz 3b402b4e36 fix(ui): collapse empty navbar topbar and add CTA spacing
Quality / quality (ubuntu-latest) (push) Failing after 11m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 18:28:39 +05:30
Clintchiz fa535575c5 fix(cli): resolve package updates on Windows
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 17:23:35 +05:30
Clintchiz 46ef4b775b fix(ui): harden public page components
Quality / quality (ubuntu-latest) (push) Failing after 10m19s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 17:13:14 +05:30
Clintchiz 7a36c1905f fix(cli): handle version flags before workspace discovery
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 15:03:38 +05:30
Clintchiz 1a8c75a958 chore: refresh editor language server bundle 2026-08-24 14:58:03 +05:30
Clintchiz b3c93e9b18 test: harden package boundaries and audit budgets
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 12:05:20 +05:30
Clintchiz 613ff7ae5b fix: invoke npm reliably from Bun on Windows
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 11:37:06 +05:30
Clintchiz e372ae571a chore: harden release checks and package coverage
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-24 11:36:13 +05:30
Clintchiz 354082ebc3 fix: recognize globally registered components
Quality / quality (ubuntu-latest) (push) Failing after 10m22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 23:03:45 +05:30
Clintchiz aa21bc4a18 chore: release cli with fixed dev runtime
Quality / quality (ubuntu-latest) (push) Failing after 11m3s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 22:58:09 +05:30
Clintchiz 1be482caab fix: defer reactive props in branch pre-rendering
Quality / quality (ubuntu-latest) (push) Failing after 10m4s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 22:51:25 +05:30
Clintchiz 2d1cda0eab chore: restore production release gate 2026-08-23 22:35:11 +05:30
Clintchiz d26403fce2 chore: update public API baseline 2026-08-23 22:24:51 +05:30
Clintchiz f52e1d50e4 feat: centralize mail credential and sandbox policy
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 21:51:37 +05:30
Clintchiz fe44bc2091 fix: make payment event reduction monotonic
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 21:34:59 +05:30
Clintchiz 98e0813061 feat: add gateway-neutral payment package
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 21:05:27 +05:30
Clintchiz a9670c2a1c fix: close durable queue and runtime gaps
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:47:00 +05:30
Clintchiz 1a94179b5f fix: synchronize forwarded component props
Quality / quality (ubuntu-latest) (push) Failing after 10m57s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:20:43 +05:30
Clintchiz f13e83fd74 fix: stage CLI release dependencies
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:09:26 +05:30
Clintchiz 7351ed1566 chore: release synchronized client runtime
Quality / quality (ubuntu-latest) (push) Failing after 10m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 20:07:18 +05:30
Clintchiz 53b9947ef9 fix: synchronize mounted component props
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 19:42:39 +05:30
Clintchiz 56cde5aaf8 fix(authz): reserve role inheritance namespace
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 19:35:03 +05:30
Clintchiz 4be4b2c346 fix: expose client fetch and signed adjustments
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 18:56:05 +05:30
ClintchizandClaude Opus 5 6258495b67 docs: propose @wrnexus/payment with a multi-gateway adapter system
Gateways are adapters behind one interface, with capabilities DECLARED rather
than assumed -- because gateways are not interchangeable. Some have no
authorize-then-capture, some cannot refund partially, some have no vault. An
interface that pretends otherwise fails at the moment money should have moved.
So capabilities are declared, refused loudly when absent, and checked at build
time where the gateway is statically known.

Tier 1 is sandbox, Stripe, Razorpay and PayPal. Stripe and Razorpay are
deliberately the first real pair because they DIFFER on capture model, currency
spread and refund semantics -- one gateway does not prove an abstraction, and
two similar ones prove it badly. Tier 2 and a regional Tier 3 follow, and
defineGateway() makes a third-party adapter a first-class citizen held to the
same shared contract suite.

Two rules shape the package: it never touches a raw card number (hosted fields
keep an application in PCI SAQ-A rather than SAQ-D), and the signed webhook is
the source of truth rather than the browser redirect, which is a claim from an
untrusted client.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:47:41 +05:30
149 changed files with 6897 additions and 577 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
bun-version: 1.4.0
- uses: actions/setup-node@v4
with:
node-version: 24
+116 -102
View File
@@ -9,7 +9,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/bun": "^1.3.14",
"@types/bun": "^1.4.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"eslint": "^10.8.1",
@@ -133,7 +133,7 @@
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/bun": "^1.3.14",
"@wrnexus/cli": "^0.8.35",
"@wrnexus/cli": "^0.8.36",
"eslint": "^10.8.1",
"prettier": "^3.9.6",
"typescript": "^6.0.3",
@@ -145,27 +145,27 @@
"version": "0.1.0",
"dependencies": {
"@wrnexus/ai": "^0.8.9",
"@wrnexus/auth": "^0.8.11",
"@wrnexus/auth": "^0.8.12",
"@wrnexus/authz": "^0.8.9",
"@wrnexus/captcha": "^0.8.10",
"@wrnexus/captcha": "^0.8.11",
"@wrnexus/core": "^0.8.9",
"@wrnexus/csr": "^0.8.21",
"@wrnexus/db": "^0.8.14",
"@wrnexus/dev-server": "^0.8.32",
"@wrnexus/encryption": "^0.8.8",
"@wrnexus/db": "^0.8.15",
"@wrnexus/dev-server": "^0.8.33",
"@wrnexus/encryption": "^0.8.9",
"@wrnexus/helpers": "^0.8.8",
"@wrnexus/i18n": "^0.8.10",
"@wrnexus/image": "^0.8.9",
"@wrnexus/jwt": "^0.8.8",
"@wrnexus/i18n": "^0.8.11",
"@wrnexus/image": "^0.8.10",
"@wrnexus/jwt": "^0.8.9",
"@wrnexus/observability": "^0.8.8",
"@wrnexus/realtime": "^0.8.9",
"@wrnexus/realtime": "^0.8.10",
"@wrnexus/security": "^0.8.8",
"@wrnexus/store": "^0.8.8",
"@wrnexus/styles": "^0.8.15",
"@wrnexus/tracking": "^0.8.8",
"@wrnexus/ui": "^0.8.19",
"@wrnexus/uploader": "^0.8.9",
"@wrnexus/validation": "^0.8.9",
"@wrnexus/uploader": "^0.8.10",
"@wrnexus/validation": "^0.8.10",
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -173,7 +173,7 @@
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14",
"@wrnexus/cli": "^0.8.35",
"@wrnexus/cli": "^0.8.36",
"eslint": "^10.8.1",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.3",
@@ -186,27 +186,27 @@
"version": "0.1.0",
"dependencies": {
"@wrnexus/ai": "^0.8.9",
"@wrnexus/auth": "^0.8.11",
"@wrnexus/auth": "^0.8.12",
"@wrnexus/authz": "^0.8.9",
"@wrnexus/captcha": "^0.8.10",
"@wrnexus/captcha": "^0.8.11",
"@wrnexus/core": "^0.8.9",
"@wrnexus/csr": "^0.8.21",
"@wrnexus/db": "^0.8.14",
"@wrnexus/dev-server": "^0.8.32",
"@wrnexus/encryption": "^0.8.8",
"@wrnexus/db": "^0.8.15",
"@wrnexus/dev-server": "^0.8.33",
"@wrnexus/encryption": "^0.8.9",
"@wrnexus/helpers": "^0.8.8",
"@wrnexus/i18n": "^0.8.10",
"@wrnexus/image": "^0.8.9",
"@wrnexus/jwt": "^0.8.8",
"@wrnexus/i18n": "^0.8.11",
"@wrnexus/image": "^0.8.10",
"@wrnexus/jwt": "^0.8.9",
"@wrnexus/observability": "^0.8.8",
"@wrnexus/realtime": "^0.8.9",
"@wrnexus/realtime": "^0.8.10",
"@wrnexus/security": "^0.8.8",
"@wrnexus/store": "^0.8.8",
"@wrnexus/styles": "^0.8.15",
"@wrnexus/tracking": "^0.8.8",
"@wrnexus/ui": "^0.8.19",
"@wrnexus/uploader": "^0.8.9",
"@wrnexus/validation": "^0.8.9",
"@wrnexus/uploader": "^0.8.10",
"@wrnexus/validation": "^0.8.10",
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -214,7 +214,7 @@
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14",
"@wrnexus/cli": "^0.8.35",
"@wrnexus/cli": "^0.8.36",
"eslint": "^10.8.1",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.3",
@@ -235,7 +235,7 @@
},
"packages/auth": {
"name": "@wrnexus/auth",
"version": "0.8.17",
"version": "0.8.18",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*",
@@ -257,7 +257,7 @@
},
"packages/authz": {
"name": "@wrnexus/authz",
"version": "0.8.14",
"version": "0.8.15",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
@@ -277,7 +277,7 @@
},
"packages/captcha": {
"name": "@wrnexus/captcha",
"version": "0.8.11",
"version": "0.8.14",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -292,7 +292,7 @@
},
"packages/cli": {
"name": "@wrnexus/cli",
"version": "0.8.57",
"version": "0.8.64",
"bin": {
"wrnexus": "src/index.ts",
},
@@ -320,7 +320,7 @@
},
"packages/compiler": {
"name": "@wrnexus/compiler",
"version": "0.8.19",
"version": "0.8.22",
"dependencies": {
"@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*",
@@ -342,7 +342,7 @@
},
"packages/csr": {
"name": "@wrnexus/csr",
"version": "0.8.31",
"version": "0.8.35",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
@@ -357,7 +357,7 @@
},
"packages/dev-server": {
"name": "@wrnexus/dev-server",
"version": "0.8.50",
"version": "0.8.56",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*",
@@ -372,6 +372,7 @@
"@wrnexus/pubsub": "workspace:*",
"@wrnexus/pwa": "workspace:*",
"@wrnexus/queue": "workspace:*",
"@wrnexus/react": "workspace:*",
"@wrnexus/router": "workspace:*",
"@wrnexus/rpc": "workspace:*",
"@wrnexus/security": "workspace:*",
@@ -385,7 +386,7 @@
},
"packages/dev-toolbar": {
"name": "@wrnexus/dev-toolbar",
"version": "0.8.13",
"version": "0.8.16",
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^6.0.3",
@@ -487,8 +488,9 @@
},
"packages/mail": {
"name": "@wrnexus/mail",
"version": "0.8.1",
"version": "0.8.3",
"dependencies": {
"@wrnexus/encryption": "workspace:*",
"@wrnexus/queue": "workspace:*",
},
},
@@ -501,7 +503,7 @@
},
"packages/metering": {
"name": "@wrnexus/metering",
"version": "0.8.2",
"version": "0.8.3",
},
"packages/mobile": {
"name": "@wrnexus/mobile",
@@ -528,6 +530,14 @@
"@wrnexus/core": "workspace:*",
},
},
"packages/payment": {
"name": "@wrnexus/payment",
"version": "0.8.1",
"dependencies": {
"@wrnexus/db": "workspace:*",
"@wrnexus/plugin": "workspace:*",
},
},
"packages/playground": {
"name": "@wrnexus/playground",
"version": "0.8.8",
@@ -561,7 +571,7 @@
},
"packages/queue": {
"name": "@wrnexus/queue",
"version": "0.8.13",
"version": "0.8.14",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
@@ -645,11 +655,11 @@
},
"packages/store": {
"name": "@wrnexus/store",
"version": "0.8.9",
"version": "0.8.12",
},
"packages/styles": {
"name": "@wrnexus/styles",
"version": "0.8.19",
"version": "0.8.20",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -658,11 +668,11 @@
},
"packages/syntax": {
"name": "@wrnexus/syntax",
"version": "0.8.13",
"version": "0.8.18",
},
"packages/test": {
"name": "@wrnexus/test",
"version": "0.8.11",
"version": "0.8.12",
},
"packages/tracking": {
"name": "@wrnexus/tracking",
@@ -670,7 +680,7 @@
},
"packages/typecheck": {
"name": "@wrnexus/typecheck",
"version": "0.8.13",
"version": "0.8.16",
"dependencies": {
"@wrnexus/syntax": "workspace:*",
"typescript": "^6.0.3",
@@ -678,14 +688,14 @@
},
"packages/ui": {
"name": "@wrnexus/ui",
"version": "0.8.23",
"version": "0.8.56",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/uploader": {
"name": "@wrnexus/uploader",
"version": "0.8.10",
"version": "0.8.13",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -786,7 +796,7 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
@@ -812,7 +822,7 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.123", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-0CozmpKXEOEEhltrfT2zt+/t1hez67Y+hM2VJWoIcBKdBrb83VYuKf+b5A0QU9HfQm+RNn2GjFWlTOwi+Ss6IA=="],
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.125", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-tOCk1QKMtKnCfPAgZRHgjRkQTP7wF5IO+iPKvvp8vxGZYPkSLhx4HTV3Ng0pIZ3wNWrS6kVpHkunJ1dc19L1og=="],
"@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="],
@@ -832,6 +842,8 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
@@ -862,79 +874,79 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA=="],
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "@peculiar/asn1-x509-attr": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw=="],
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg=="],
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw=="],
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ=="],
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ=="],
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-rsa": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg=="],
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.9.4", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.4", "@peculiar/asn1-pkcs8": "^2.9.4", "@peculiar/asn1-rsa": "^2.9.4", "@peculiar/asn1-schema": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw=="],
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA=="],
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ=="],
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pfx": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ=="],
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.9.4", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.4", "@peculiar/asn1-pfx": "^2.9.4", "@peculiar/asn1-pkcs8": "^2.9.4", "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "@peculiar/asn1-x509-attr": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ=="],
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg=="],
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="],
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.4", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg=="],
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg=="],
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q=="],
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA=="],
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg=="],
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
"@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="],
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
@@ -966,7 +978,7 @@
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
@@ -974,11 +986,11 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
"@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
"@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="],
"@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="],
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
@@ -1066,6 +1078,8 @@
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
"@wrnexus/payment": ["@wrnexus/payment@workspace:packages/payment"],
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
@@ -1108,7 +1122,7 @@
"@wrnexus/validation": ["@wrnexus/validation@workspace:packages/validation"],
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -1134,7 +1148,7 @@
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
@@ -1166,7 +1180,7 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -1188,7 +1202,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.8.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ=="],
"eslint": ["eslint@10.9.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw=="],
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
@@ -1224,7 +1238,7 @@
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -1232,7 +1246,7 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"happy-dom": ["happy-dom@20.11.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw=="],
"happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="],
"i18n-showcase": ["i18n-showcase@workspace:examples/i18n-showcase"],
@@ -1304,7 +1318,7 @@
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
@@ -1340,7 +1354,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
@@ -1358,7 +1372,7 @@
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
"pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="],
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
@@ -1370,9 +1384,9 @@
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
"rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="],
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -1448,27 +1462,27 @@
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
"@antfu/install-pkg/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
"@antfu/install-pkg/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
@@ -0,0 +1,354 @@
# `@wrnexus/payment` — proposal
**Status:** draft for review
**Date:** 2026-08-23
One package that gives an application real payments: gateway configuration, initialisation,
capture, refunds, stored methods, an admin surface, and UI components — so a team never writes
this again.
## The one rule that shapes everything
**The package never touches a raw card number.**
Every supported gateway offers hosted fields or a hosted checkout that tokenises the card in
the customer's browser, against the gateway's domain, before anything reaches your server. That
is what keeps an application in PCI **SAQ-A** — roughly a self-assessment questionnaire —
instead of SAQ-D, which is an audit programme with a six-figure floor.
So the package's UI components mount the gateway's own fields. `PayNow` renders a button that
opens a gateway session; it never renders an `<input name="cardNumber">`, and the package
exposes no API that would accept one. A framework that makes the cheap wrong thing easy will
have it done by someone in a hurry.
## The second rule: the webhook is the truth
A browser redirect saying "payment succeeded" is a claim from an untrusted client. The
authoritative event is the gateway's signed webhook.
So `checkPayment()` reads local state that webhooks maintain, and the local record is only ever
advanced by a verified webhook or a server-side gateway query — never by a client callback. The
redirect is a UX affordance: it tells the customer where to look, not the system what happened.
This is the single most common way a payment integration leaks money, and it is a design
decision, not a runtime check.
## Money model — the same discipline as metering
Payments are **append-only**, exactly like `@wrnexus/metering`'s ledger, and for the same
reason: a balance you cannot explain is a balance nobody trusts.
| Table | Holds |
| ---------------------- | ----------------------------------------------------------------------------------- |
| `wrn_payment_intent` | one row per attempt: amount, currency, gateway, status, idempotency key |
| `wrn_payment_event` | append-only; every webhook and state transition, with the raw signed payload |
| `wrn_payment_refund` | one row per refund attempt, linked to its intent |
| `wrn_payment_method` | stored gateway tokens — never card data, never a PAN, at most a brand and last four |
| `wrn_payment_customer` | the mapping from your user id to each gateway's customer id |
A payment's current status is derived from its events, not from a mutable column that a race can
clobber. The `status` column on the intent is a cache of that derivation, and the package ships
a reconciliation command that recomputes it and reports disagreements.
**Idempotency is mandatory, not optional.** Every initialise and every refund carries a key; a
repeat with the same key returns the original result rather than charging twice. This is where a
double-click becomes a double-charge, and it should be impossible to opt out of.
## Gateways are adapters
```ts
export interface PaymentGateway<TConfig = unknown> {
readonly id: string; // "stripe" | "razorpay" | "paypal" | ...
readonly capabilities: GatewayCapabilities;
readonly supports: { currencies: string[] | "any"; countries: string[] | "any" };
createIntent(input: CreateIntentInput): Promise<GatewayIntent>;
fetchIntent(intentRef: string): Promise<GatewayIntent>;
cancelIntent(intentRef: string, reason: string): Promise<GatewayIntent>;
refund(input: RefundInput): Promise<GatewayRefund>;
verifyWebhook(req: Request, secret: string): Promise<WebhookEvent>; // signature check
capture?(intentRef: string, amount?: Money): Promise<GatewayIntent>; // authorize-then-capture
listMethods?(customerRef: string): Promise<GatewayMethod[]>;
attachMethod?(customerRef: string, token: string): Promise<GatewayMethod>;
detachMethod?(methodRef: string): Promise<void>;
createCustomer?(input: CustomerInput): Promise<string>;
readonly clientConfig: (intent: GatewayIntent) => Record<string, string>; // safe to send
}
```
`clientConfig` exists so the boundary is explicit: it returns exactly what the browser may see —
a publishable key and a session id, never a secret. Anything not returned by it cannot reach the
client, which is a structural guarantee rather than a code-review habit.
### Capabilities are declared, and the difference is refused loudly
This is the part that decides whether a multi-gateway abstraction holds. **Gateways are not
interchangeable.** Some have no authorise-then-capture. Some cannot do partial refunds. Some
have no stored-method vault. Some only settle in one currency.
An interface that pretends otherwise produces the worst failure mode there is: an application
calls `capturePayment()` against a gateway that has no such concept, and gets a confusing
gateway error at the moment money should have moved.
So every adapter declares what it can do, and the package refuses the rest **at call time with a
named reason** — and, better, at **build time** where the gateway is statically known:
```ts
export interface GatewayCapabilities {
authorizeThenCapture: boolean; // Stripe yes, Razorpay effectively no
partialRefund: boolean;
multipleRefunds: boolean;
storedMethods: boolean;
customerVault: boolean;
hostedFields: boolean; // inline hosted inputs
hostedCheckout: boolean; // full redirect/modal
webhookSignature: boolean; // an adapter without this is refused in production
payouts: boolean;
disputes: boolean;
}
```
`capabilitiesOf("razorpay").partialRefund` is a real answer an application can branch on, and
`RefundButton` reads it to decide whether to offer an amount field or only a full refund. A
capability an adapter does not declare is not merely unimplemented — it is refused, with a
message naming the gateway and the capability.
**`webhookSignature: false` is refused outright in production.** An adapter that cannot verify a
webhook cannot be trusted to tell you money moved, and the whole design rests on that.
### The roster
**Tier 1 — ship first, fully covered by the contract suite:**
| Adapter | Notes |
| ---------- | ---------------------------------------------------------------------------------- |
| `sandbox` | behaves like a real gateway, signed webhooks, injectable failure modes, no network |
| `stripe` | global; hosted fields, auth-then-capture, vault, partial and multiple refunds |
| `razorpay` | India-first; hosted checkout, INR-centric, no true auth-then-capture |
| `paypal` | global; hosted checkout, its own order/capture model |
Two real gateways prove the abstraction; one does not. Stripe and Razorpay are deliberately the
first pair because they **differ** — capture model, currency spread, refund semantics — so the
capability system is exercised rather than assumed.
**Tier 2 — same contract, added after the abstraction has survived Tier 1:**
`adyen`, `square`, `braintree`, `mollie`, `checkout.com`, `paddle` (merchant-of-record, so tax
and invoicing differ meaningfully).
**Tier 3 — regional, community-shaped:**
`payu`, `cashfree`, `phonepe`, `paytm` (India); `paystack`, `flutterwave` (Africa);
`midtrans`, `xendit` (South-East Asia); `authorize.net` (US legacy); `mercadopago` (LatAm).
### Any developer can add one
The roster is a starting set, not a ceiling. A custom adapter is a first-class citizen:
```ts
import { defineGateway } from "@wrnexus/payment";
export const acme = defineGateway({
id: "acme",
capabilities: { partialRefund: true, storedMethods: false, webhookSignature: true /* … */ },
supports: { currencies: ["INR", "USD"], countries: ["IN"] },
async createIntent(input) {
/* … */
},
async verifyWebhook(req, secret) {
/* … */
},
// …
});
```
`defineGateway` validates the shape at registration and runs the **shared contract suite** in
tests, so a third-party adapter is held to exactly the standard the built-in ones are. An adapter
that passes the suite behaves identically to Stripe's from the application's point of view; one
that does not, fails in CI rather than in production.
## Configuration
Config is **per-gateway and typed** — each adapter declares its own shape, so a missing
`webhookSecret` is a type error, not a 3am discovery:
```ts
// wrnexus.config.ts
payment: {
default: "stripe",
currency: "INR",
gateways: {
stripe: {
secretKey: env("STRIPE_SECRET_KEY"),
publishableKey: env("STRIPE_PUBLISHABLE_KEY"),
webhookSecret: env("STRIPE_WEBHOOK_SECRET"),
apiVersion: "2026-03-31",
captureMethod: "automatic", // or "manual" for auth-then-capture
},
razorpay: {
keyId: env("RAZORPAY_KEY_ID"),
keySecret: env("RAZORPAY_KEY_SECRET"),
webhookSecret: env("RAZORPAY_WEBHOOK_SECRET"),
theme: { color: "#0e7c86" },
},
acme: { apiKey: env("ACME_KEY"), webhookSecret: env("ACME_WEBHOOK_SECRET") },
},
// Optional: pick a gateway per payment instead of always using the default.
route(intent) {
if (intent.currency === "INR") return "razorpay";
if (intent.country === "US") return "stripe";
return "stripe";
},
sandbox: process.env.NODE_ENV !== "production",
}
```
Selection has three levels, most specific winning: an explicit `gateway` on the call, then
`route()`, then `default`. A `route()` that returns a gateway which is not configured, or which
cannot settle the intent's currency, is a startup error where it can be detected statically and
a named refusal where it cannot.
**Registering a custom adapter is a config line, not a fork:**
```ts
import { acme } from "./payments/acme.ts";
payment: { adapters: [acme], default: "acme", /* … */ }
```
**Refuse to boot on a misconfiguration rather than failing at the first payment.** A live secret
key with `sandbox: true`, or a missing webhook secret, is a startup error naming the variable —
the same lesson as `APP_ENCRYPTION_KEY` failing as an opaque WebCrypto error until it was made
explicit.
## The helper surface
```ts
// Lifecycle
initializePayment(input): Promise<PaymentIntent> // amount, currency, subject, metadata, idempotencyKey
confirmPayment(id): Promise<PaymentIntent> // server-side confirm where the gateway needs it
capturePayment(id, amount?): Promise<PaymentIntent>// for auth-then-capture flows
cancelPayment(id, reason): Promise<PaymentIntent>
checkPayment(id): Promise<PaymentStatus> // derived from events, never from a client claim
syncPayment(id): Promise<PaymentIntent> // authoritative re-read from the gateway
// Refunds
refundPayment({ id, amount?, reason, idempotencyKey }): Promise<Refund> // partial by default
listRefunds(id): Promise<Refund[]>
refundableAmount(id): Promise<Money> // amount minus refunds already settled
// Stored methods
paymentMethods(userId): Promise<PaymentMethod[]>
attachPaymentMethod(userId, token): Promise<PaymentMethod>
detachPaymentMethod(methodId): Promise<void>
setDefaultPaymentMethod(userId, methodId): Promise<void>
// Records and reporting
getPayment(id) / listPayments(filter) // filter by user, status, gateway, date range
paymentTotals(filter): Promise<{ captured, refunded, net, byCurrency }>
reconcilePayments(range): Promise<Discrepancy[]> // local vs gateway, the operator's safety net
// Webhooks
paymentWebhookHandler(gatewayId): RouteHandler // signature-verified, idempotent, replay-safe
```
Every function that moves money is **non-throwing and returns a result** with a `fault`
discriminator, matching `@wrnexus/metering`: a refusal's reason is safe to show a customer, a
fault's reason belongs only in the log. That distinction was learned the hard way — a naive
catch once answered "payment required" with a raw SQLite message.
### `refundableAmount` earns its place
Partial refunds are where integrations quietly go wrong: two concurrent partial refunds each
check "is there enough left?", both see yes, and together exceed the capture. `refundPayment`
must enforce the cap with a conditional write — the same shape as metering's reserve — not with
a read-then-write.
## Migrations ship with the package
`@wrnexus/authz` provisioning nothing is a real cost in this codebase: every application
hand-wires `ensureAuthzTables`, and the framework's own example copies DDL by hand and silently
drifts. Payment must not repeat that.
The package is a **plugin**. It contributes its migrations, its webhook route, and its authz
permissions (`payment:read`, `payment:refund`, `payment:configure`) on install. An application
adds a config block and gets a working, guarded, migrated payment system.
## UI components
Every component mounts gateway-hosted fields; none collects card data itself.
| Component | Does |
| ------------------- | -------------------------------------------------------------------------------- |
| `PayNow` | the button: opens a gateway session, shows pending/success/failure, emits `paid` |
| `PaymentSheet` | hosted fields inline, with the gateway's own validation surfaced |
| `PaymentMethodList` | stored methods, set-default, detach — brand and last four only |
| `PaymentStatus` | live status for one intent, driven by `checkPayment` |
| `RefundButton` | admin-side, requires a reason, shows `refundableAmount` |
| `PaymentHistory` | a customer's payments and refunds |
| `PaymentSummary` | totals by status and currency for a range |
`PayNow` needs to survive the customer closing the tab mid-payment, so its resolved state comes
from `checkPayment`, not from whether the callback fired.
**One caveat to state plainly:** these components will hit `GAP-01` today. A `PaymentStatus`
that first appears client-side renders as an empty placeholder, and a `PayNow` whose props change
after mount will not update. Until the client component runtime lands, these components must be
built server-rendered-first with real navigation, exactly as the Sendline admin console was.
## Admin surface
A payments console — list and filter, view one payment with its full event timeline, issue a
refund with a required reason, inspect webhook deliveries and replay a failed one, and run
reconciliation. Every privileged action writes an audit entry, so the package should either take
a dependency on an audit interface or define one.
## Verification — the standard this project now holds
Unit and integration tests are necessary and insufficient. The package is done when:
1. Every gateway adapter — built-in **and** third-party — passes one shared contract suite, so
behaviour cannot drift per gateway. An adapter declaring a capability it does not honour fails
the suite; an adapter honouring one it does not declare fails too, because a silent extra is
how an application comes to depend on something the next gateway lacks.
2. The sandbox adapter can drive a full lifecycle offline: initialise, webhook, capture, partial
refund, over-refund refused, reconciliation clean.
3. **A real payment is driven end to end in a browser against a gateway's test mode**, and the
money is confirmed in the gateway's own dashboard — not in our database. Phase 3 proved that a
row saying `sent` is not the same as an email arriving; a row saying `captured` is not the same
as money moving.
4. A replayed webhook, a duplicated initialise, and a double-clicked refund each change the
ledger exactly once.
5. Reconciliation over a deliberately corrupted local row reports the discrepancy rather than
hiding it.
## What this package deliberately does not do
- **No card data, ever.** No PAN, no CVV, no expiry, in any API, table, log or component.
- **No invented gateway.** The sandbox adapter is clearly a sandbox and says so in its UI.
- **No subscription billing in v1.** Recurring is a genuinely separate problem — plans, proration,
dunning, retries — and bolting it on would compromise both.
- **No currency conversion.** Store and settle in the currency charged; a converted number in a
ledger is a number nobody can reconcile.
- **No silent capture of an expired authorisation.** It fails loudly.
## Build order
1. Core: schema, append-only events, status derivation, idempotency, non-throwing results
2. The capability system, `defineGateway`, and the shared adapter contract suite
3. The sandbox adapter — first consumer of the contract suite, and the thing every later
adapter is checked against
4. **Stripe, then Razorpay.** Deliberately this pair, because they differ on capture model,
currency spread and refund semantics. Building them together is what stops the abstraction
quietly becoming "Stripe, with names changed"
5. Gateway selection: explicit, `route()`, default — with the misconfiguration errors
6. The webhook route: signature verification, replay safety, event storage
7. Refunds, including the concurrent partial-refund cap and the `partialRefund: false` path
8. Stored methods and customers, behind `storedMethods`
9. PayPal — the third gateway, and the real test of whether Tier 2 can be added by someone who
did not design the abstraction
10. Admin console and reconciliation
11. UI components, capability-aware
12. Browser verification against a gateway test mode
+182 -2
View File
@@ -968,6 +968,7 @@
},
"@wrnexus/cli": {
".": [],
"./test-setup": [],
"./workspace": [
"ResolvedWorkspaceApp",
"ResolvedWorkspaceConfig",
@@ -1352,6 +1353,8 @@
"ActionClientError",
"ActionClientOptions",
"ActionResult",
"ApiClientOptions",
"ApiRequest",
"ClientModuleScope",
"HydrationScopeApi",
"NAV_RUNTIME",
@@ -1365,6 +1368,7 @@
"callServerFunction",
"collectRefs",
"createActionClient",
"createApiClient",
"createOutputProxy",
"createServerProxy",
"getActionRuntime",
@@ -1375,11 +1379,13 @@
"invalidateClientModule",
"invokeOutput",
"loadClientFunctions",
"registerOutputHandler"
"registerOutputHandler",
"useFetch"
]
},
"@wrnexus/db": {
".": [
"AddSeedOptions",
"BaseType",
"Column",
"ColumnDef",
@@ -1390,6 +1396,7 @@
"Dialect",
"Driver",
"ExecResult",
"LedgerOptions",
"Migration",
"MigrationRunOptions",
"MigrationSafetyIssue",
@@ -1406,7 +1413,13 @@
"RelationOptions",
"Repository",
"Row",
"SeedDatabase",
"SeedRow",
"SeedStep",
"SeedUserAccount",
"TxHandle",
"UpsertSeedOptions",
"addSeedData",
"analyzeMigrationSafety",
"analyzeMigrations",
"appliedMigrations",
@@ -1421,6 +1434,8 @@
"cursorPaginate",
"databaseHealth",
"databaseNames",
"defineLedger",
"defineSeed",
"exists",
"firstOrThrow",
"generateQueriesFile",
@@ -1438,15 +1453,20 @@
"queryOperation",
"registerDb",
"registerLazyDb",
"removeSeedData",
"resetDbPerformanceSnapshot",
"retryTransaction",
"rollback",
"runSeedQuery",
"scaffoldMigration",
"seedIfMissing",
"seedUsers",
"setDb",
"softDeleteClause",
"status",
"table",
"tenantScope",
"upsertSeedData",
"v",
"withTransaction"
],
@@ -1647,6 +1667,7 @@
"EncryptionKey",
"EncryptionKeyring",
"ReplayStore",
"SecretResourceOptions",
"createEncryptedRequest",
"createKeyring",
"createMemoryReplayStore",
@@ -1654,6 +1675,7 @@
"decryptEncryptedResponse",
"decryptHttpBody",
"decryptRequest",
"defineSecretResource",
"deriveKey",
"encrypt",
"encryptHttpBody",
@@ -1687,26 +1709,39 @@
"@wrnexus/helpers": {
".": [
"AllowedHosts",
"ApiHandler",
"HttpError",
"LoginRedirectOptions",
"OriginalRequestOptions",
"RequestContext",
"ResourceDefinition",
"ResourceHandlers",
"RetryOptions",
"appOrigin",
"appUrl",
"authorized",
"backoffDelay",
"clamp",
"currentAppName",
"currentAppOrigin",
"defineApiRoute",
"defineResource",
"getOriginalRequestMethod",
"getOriginalRequestOrigin",
"getOriginalRequestPath",
"getOriginalRequestUrl",
"json",
"once",
"redirectToLogin",
"requireJson",
"requireParam",
"requirePermission",
"requireUser",
"retry",
"safeJsonParse",
"sleep",
"stableStringify",
"subjectId",
"withTimeout",
"workspaceAppOrigins",
"workspaceRootDomain"
@@ -1902,6 +1937,25 @@
"isApiReferenceAt"
]
},
"@wrnexus/mail": {
".": [
"MailDriver",
"MailMessage",
"MailOptions",
"MailSandboxDecision",
"MailTemplate",
"SealedMailCredentialsOptions",
"SealedMailDriverOptions",
"defineMail",
"defineMailTemplate",
"defineSealedMailCredentials",
"interpolateTemplate",
"mailSandboxDecision",
"queuedMail",
"sealedMailDriver",
"storedMailTemplate"
]
},
"@wrnexus/mcp": {
".": [
"McpServer",
@@ -1913,6 +1967,20 @@
"runMcpStdio"
]
},
"@wrnexus/metering": {
".": [
"EntitlementPlan",
"EntitlementsOptions",
"MeterKind",
"MeterOptions",
"MeterPack",
"MeterResult",
"MeterStore",
"defineEntitlements",
"defineMeter",
"definePacks"
]
},
"@wrnexus/mobile": {
".": [
"CapacitorBridge",
@@ -2113,6 +2181,70 @@
"traceMiddleware"
]
},
"@wrnexus/payment": {
".": [
"CreateIntentInput",
"CustomerInput",
"GatewayCapabilities",
"GatewayContractFixture",
"GatewayContractReport",
"GatewayIntent",
"GatewayMethod",
"GatewayRefund",
"GatewaySupport",
"InitializePaymentInput",
"Money",
"PayPalConfig",
"PaymentEvent",
"PaymentGateway",
"PaymentIntent",
"PaymentMethod",
"PaymentOptions",
"PaymentRefund",
"PaymentResult",
"PaymentService",
"PaymentStatus",
"PaymentStore",
"RazorpayConfig",
"RefundInput",
"SandboxOptions",
"StripeConfig",
"WebhookEvent",
"configurePayment",
"databasePaymentStore",
"defineGateway",
"definePayment",
"derivePaymentStatus",
"exerciseGatewayContract",
"getPaymentService",
"memoryPaymentStore",
"paymentPlugin",
"paypalGateway",
"razorpayGateway",
"sandboxGateway",
"signSandboxWebhook",
"stripeGateway",
"verifyGatewayContract"
],
"./adapters": [
"PayPalConfig",
"RazorpayConfig",
"SandboxOptions",
"StripeConfig",
"paypalGateway",
"razorpayGateway",
"sandboxGateway",
"signSandboxWebhook",
"stripeGateway"
],
"./plugin": [
"default",
"paymentPlugin"
],
"./runtime-webhook": [
"POST"
]
},
"@wrnexus/playground": {
".": [
"PlaygroundCompilation",
@@ -2277,21 +2409,41 @@
"@wrnexus/queue": {
".": [
"AddOptions",
"DefineQueueOptions",
"DefinedJob",
"DefinedJobOptions",
"DefinedJobs",
"DefinedQueue",
"DurableQueue",
"DurableQueueOptions",
"Job",
"JobContext",
"JobDefinition",
"JobHandler",
"JobStateMachineOptions",
"JobStatus",
"OutboxEntry",
"OutboxWriter",
"POSTGRES_QUEUE_SCHEMA",
"Queue",
"QueueAdminOptions",
"QueueDashboardSnapshot",
"QueueEvent",
"QueueEventType",
"QueueHealth",
"QueueJobDefinitions",
"QueueJobRecord",
"QueueJobState",
"QueueOptions",
"QueueScheduler",
"QueueStorage",
"QueueStorageConfig",
"QueueStore",
"RedisQueueClient",
"SagaStep",
"ScheduledJob",
"SqlQueueClient",
"SqliteQueueClient",
"SubjectJob",
"SubjectQueue",
"WorkerDefinition",
@@ -2303,24 +2455,46 @@
"WorkflowStep",
"WorkflowStore",
"addBatch",
"configureQueueStorage",
"configuredQueueStore",
"createDurableQueue",
"createQueue",
"createQueueAdminHandler",
"createQueueScheduler",
"createWorkflowEngine",
"cronToInterval",
"databaseQueueStore",
"defineDurableWorkflow",
"defineJob",
"defineJobStateMachine",
"defineQueue",
"defineSaga",
"defineWorker",
"defineWorkflow",
"drainOutbox",
"installDatabaseQueueSchema",
"installOutboxSchema",
"installSqliteQueueSchema",
"memoryQueueStore",
"memoryWorkflowStore",
"postgresQueueStore",
"queueDashboardSnapshot",
"queueStorageConfig",
"redisQueueStore",
"renderQueueDashboard",
"runQueueDaemon",
"runWorker",
"subjectQueue"
"sqliteQueueSchema",
"sqliteQueueStore",
"subjectQueue",
"testQueue",
"withOutbox"
],
"./sqlite": [
"SqliteQueueClient",
"installSqliteQueueSchema",
"sqliteQueueSchema",
"sqliteQueueStore"
]
},
"@wrnexus/react": {
@@ -2363,6 +2537,7 @@
".": [
"AnimationTimeline",
"Cleanup",
"FormState",
"HistorySignal",
"ReactiveContext",
"ReactiveScope",
@@ -2389,6 +2564,7 @@
"transition",
"untrack",
"urlSignal",
"useForm",
"watch"
]
},
@@ -2916,11 +3092,14 @@
".": [
"BrowserArtifactPage",
"Deferred",
"DetectMutationsOptions",
"FetchMock",
"Harness",
"HarnessOptions",
"JsonResponse",
"MemoryCookieJar",
"MutationCase",
"MutationReport",
"TestRequestOptions",
"TransactionalDatabase",
"WaitForOptions",
@@ -2938,6 +3117,7 @@
"createTestContext",
"deferred",
"describe",
"detectMutations",
"expect",
"expectProblem",
"it",
+30 -30
View File
@@ -4,22 +4,22 @@
"artifacts": {
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b",
"packages/ui/components/AnnouncementBar.wrn": "cb66b8d0619ee798a115b43f0ad93fda93dfb46571589e7a9f0a8d65ec3c84c9",
"packages/ui/components/AnnouncementBar.wrn": "5b3219db555e0fcdb94ec41dc18186ee1b7c0cf9c260469f7fb1fc43435c28c4",
"packages/ui/components/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97",
"packages/ui/components/BackToTop.wrn": "5b760ec7941ffd8e58c5dedae1b322ee46e072e927533d7ec077b511a816e436",
"packages/ui/components/BackToTop.wrn": "d4b8d1fd37ff26c89b4276ce54ce6ac9581dc2ff8bbcbe160e072778bba76eab",
"packages/ui/components/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
"packages/ui/components/CTASection.wrn": "d0665f2aa33eea95e6f84120f739b3dd9c8723c882138bcb1a7f80dcd7909904",
"packages/ui/components/Card.wrn": "3cc2ff238bda279c169026a462236144ad11ec0046deef00ab00e001a07e8a4c",
"packages/ui/components/CTASection.wrn": "f6872b980432d5ee9d88388250be9838ccce51bd706349b283ef472ee692b237",
"packages/ui/components/Card.wrn": "be8cd6a438d1ede4c284a302b76f0fbc235a2423b83a7ced21131763fecc8a90",
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
"packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30",
"packages/ui/components/Collapse.wrn": "26df087a61c82f1aa1749785f4c9be654c4f934859c1a47acfcb8cc67eecc75d",
"packages/ui/components/ColorPicker.wrn": "557e36adbdee66d8ba9dcdc2446c13f3385a243f2766777feef8504908006c99",
"packages/ui/components/ColorPicker.wrn": "ccac7eb49c0f443b404004f1298fa905fde45c52b51466194920939c920348c9",
"packages/ui/components/Columns.wrn": "8ccd9e53f6799b09f8b996740d12e4e73ee782f6fd3e57595c6c16f788b9b812",
"packages/ui/components/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1",
"packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e",
@@ -28,7 +28,7 @@
"packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437",
"packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3",
"packages/ui/components/DataTable.wrn": "e929fdb4e59613878a03fb6a11e09c42f773c06ca7b20e43ae16a266835f2af4",
"packages/ui/components/DatePicker.wrn": "91bcfd3832d3bccc4de5402c0c06209947bde593bad4f869b4c29ee14a5c0bef",
"packages/ui/components/DatePicker.wrn": "cefbbb2abb57ad4c57864a0c65589592a20f9ea1695f9fc7693c3135dd82e71a",
"packages/ui/components/DeviceFrame.wrn": "c38d25419ee44b05d7c443cc4e854aeeb7a53a9289e374d71834de2dd2e4d979",
"packages/ui/components/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019",
"packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513",
@@ -36,15 +36,15 @@
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
"packages/ui/components/FeatureIconCard.wrn": "3b4f3fa62c6729e686886a5b848e26c255766684829cec2b715967b4e73d6f07",
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8",
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034",
"packages/ui/components/Footer.wrn": "f7c5a77064a09e244f689d42475c4c20143f4c8859cac451c10a50c14652db33",
"packages/ui/components/FeatureIconCard.wrn": "5f65d64235babc3e653a5695204412202015e6f3ffadca566fc18c7f9ce70428",
"packages/ui/components/FileInput.wrn": "611047c362458a4e2513b741cc3ef43fbe54c10ddafc13775e4ff39c9de34931",
"packages/ui/components/FileUploadProgress.wrn": "ba00557afebf60e6221385d6efcf08635789328deee6e551fc53fbb71e1f413b",
"packages/ui/components/Footer.wrn": "e3515e05ef3ef51e6dad71d67ddfe110c80fdcf69e952aecdc7973e69d4f08c7",
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
"packages/ui/components/Image.wrn": "361179c478d7674c3b998891a366d73de9514fc6cc8786440c6c50b9ef065357",
"packages/ui/components/InputGroup.wrn": "30e9f2e4364542179180b6d6e796002d0ffe1e30305146749874697cde6629ea",
"packages/ui/components/InputGroup.wrn": "9e259d15207500d07ff626f09c2e2f5eaf4e471f805a7bdbec154b1a23ce0f8d",
"packages/ui/components/InputNumber.wrn": "069195cc483f3f7d6f305d03fbe5a821906a030376fafb6f0d969e4c95b68fd0",
"packages/ui/components/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9",
"packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1",
@@ -55,38 +55,38 @@
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
"packages/ui/components/MegaMenu.wrn": "6a6db166259f8f2bd7ac8fbb21dba6d915c4b936d87b19805caa78189f54730a",
"packages/ui/components/MegaMenu.wrn": "a34a84f31f7a3016cf5a607a22f8f7b2322552c550278f6e8c5e6d6cb18848a8",
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
"packages/ui/components/MetricGrid.wrn": "c71e53249835908833055b553e64a8ee55fdf11ac4605436a9581ebb87a0608b",
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
"packages/ui/components/Navbar.wrn": "01903230210cd2e5e6d9325a0708f85af9623d4118e3a53f1adb770fd545425d",
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
"packages/ui/components/Navbar.wrn": "b68a824e479ce6957afc3e9710442281cdc1a8d02cd96501fe060271f54368e4",
"packages/ui/components/PageHeader.wrn": "29a93cdca7285548b79ebe5b6421eef6dbb4d5ae8be2508fa2d681e1068ba07f",
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee",
"packages/ui/components/PreferenceSwitcher.wrn": "77229e15449182563ebf49159507139928eb74e799e4438e00e9aa81fd60f982",
"packages/ui/components/PreferenceSwitcher.wrn": "519cf911390f529fff3fd16e11fb58e79d618fe851902202dc37552ee1182395",
"packages/ui/components/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
"packages/ui/components/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b",
"packages/ui/components/RangeSlider.wrn": "e8ab41782c9d6e58638f0bfccf6e3172af4ebf552189d2199cc6c96bd1652067",
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587",
"packages/ui/components/Scrollspy.wrn": "b9c6400ee92a7c228362610a2051f37bff1d3baba1dbacd66d58397dfda5d856",
"packages/ui/components/Scrollspy.wrn": "4b452fcbd524feda9c9f2c81894ee01747a51888e3b86fafb37c5d4d192a3a51",
"packages/ui/components/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
"packages/ui/components/Section.wrn": "45b7a1977c25a6020d5cc8bab2ce9c80730e7ec2d51ad53e8c9f64cd9b5b64cd",
"packages/ui/components/Section.wrn": "78da13eb8bc1acafe8e6151cbaeed4baaac2ab860e3209f1d13c467f279d2e68",
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
"packages/ui/components/SplitHero.wrn": "843387dc7b920b3cf6a1b2b785b8653c281982e524479edc2638a89381ff3577",
"packages/ui/components/StatsBar.wrn": "c273bd6ca417a0a20e5e6f1214414f678741b73bedd885fea1037575fa8c5b95",
"packages/ui/components/SplitHero.wrn": "7127cb869d1228522d066e028818dd9e512de8b6dd8f99e3dec500e6da5a3cb0",
"packages/ui/components/StatsBar.wrn": "6fabcb4374b1e074f58ebcaba968f235c8a3409762163aea51298e2515a14435",
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
"packages/ui/components/StrongPassword.wrn": "407eabcc13affc9323a346ddba8e39c81b885a59ef7043d2bd5567d4b7553282",
"packages/ui/components/StrongPassword.wrn": "cc31cb080c4b4116c544b4340b8d740d08fec1b05563372b3280c50759416c87",
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954",
"packages/ui/components/TimePicker.wrn": "ec0860a4a82a042a65acf9aa81519a5c815b61e0b8080cf6f1c986d26ce7f0e8",
"packages/ui/components/TimePicker.wrn": "33bd7a7069b7abe858cf1617b5b950733b37a8d522469ed673cff00b4dfcad0e",
"packages/ui/components/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
"packages/ui/components/TogglePassword.wrn": "6bd4d9fa82b0c8e80178430d5a42b1b3ec9725fb62be158585f68af9338c901a",
"packages/ui/components/TogglePassword.wrn": "caa58479e5c8b1e3e504181a56a7d6bcf71d0a230010733710961c31e1cc58ab",
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
"packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
@@ -94,15 +94,15 @@
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
"packages/ui/components/button.wrn": "65570c58cb2547f1e17cd158eec7cf7b1ffc721e6976c6486c9df564627b1219",
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4",
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847",
"packages/ui/components/checkbox.wrn": "da735354b57f33946591e1bd018b521ff709bae1325e2317735824673188377a",
"packages/ui/components/input.wrn": "2f17353cbf0711ca50a935ae03ee9ee53594d7e35eb73f5df573fadb7958fac2",
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
"packages/ui/components/radio.wrn": "09425a78358de5bbd2f47482f313e80065135335b969ce5ccdd5c7cc3ea5232c",
"packages/ui/components/select.wrn": "1d8d47a74d5ab9ed58d0ba11f46910897233b96652d17f5962f5dda60bd4cf8a",
"packages/ui/components/radio.wrn": "df8e562324b6d99f5a157b0971d63bd715daa0cf6189e7b0d4df04d6f0411388",
"packages/ui/components/select.wrn": "747321d6292b8052e44e8014e87ce25f4b56d8e07f86f1061e0ed0cec7651356",
"packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648",
"packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105",
"packages/ui/components/switch.wrn": "874504e4828e9db6a570d78984c4d3a76d0ceb39d70baa877649cb3798c82c85",
"packages/ui/components/textarea.wrn": "9870d37664471a103d44435a3977bf40b087a743acd76ee4d77df9b56cccbb2b",
"packages/ui/components/switch.wrn": "243f521431ffb644089df7f629bace0c07094638dfdd27e75b2017efa9cbba4f",
"packages/ui/components/textarea.wrn": "aa3a75214e998a0b57c50e416476d6f85fa0313aaf97262f2f9222150c6c9e26",
"packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d",
"packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca",
"packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62"
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus",
"displayName": "WRNexus Language Support",
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
"version": "0.8.9",
"version": "0.8.11",
"publisher": "wrnexus",
"private": true,
"license": "SEE LICENSE IN LICENSE",
+130 -20
View File
@@ -1,7 +1,7 @@
"use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: 1e7593d6b6bd1c9b8439fc34a30f9db9f7ae64f1788365a74115170aa34cd185
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
// WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272
// WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5
// Generated with TypeScript: 6.0.3
const __nodeRequire = require;
const __path = __nodeRequire("node:path");
@@ -1148,6 +1148,31 @@ function componentEventAttribute(name) {
}
return `data-wrn-out-${name}`;
}
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
function expandBindings(attrs) {
const output = [];
for (const attr of attrs) {
if (attr.event || !attr.name.startsWith("bind:")) {
output.push(attr);
continue;
}
const target = attr.name.slice("bind:".length);
if (target !== "value" && target !== "checked") {
throw new Error(`Unknown .wrn binding '${attr.name}'. Use bind:value or bind:checked.`);
}
const expression = wholeAttributeExpression(attr.value) ?? attr.value.trim();
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) {
throw new Error(`${attr.name} requires a writable state name, received '${expression}'.`);
}
output.push({ name: target, value: `{${expression}}`, event: false });
output.push({
name: target === "checked" ? "change" : "input",
value: `${expression} = payload.${target}`,
event: true,
});
}
return output;
}
function reactiveAttrValue(raw, reactive) {
let found = false;
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
@@ -1166,6 +1191,7 @@ function reactiveAttrValue(raw, reactive) {
return found ? value : null;
}
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
attrs = expandBindings(attrs);
let bindIndex = 0;
const rendered = attrs
.map((attr) => {
@@ -1302,7 +1328,7 @@ function renderLoopBody(node, locals = []) {
return compileIfExpr(node, locals);
}
const componentTag = isComponentTag(node.tag);
const attrs = node.attrs
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => {
const name = attr.event
@@ -1562,9 +1588,9 @@ function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindin
const island = islandMarkerFor(node);
if (island)
return island;
const attrs = node.attrs
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => renderPageComponentAttr(attr, loops))
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
.join("");
const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
@@ -1577,7 +1603,7 @@ function renderNestedComponentInvocation(node, ctx) {
if (island)
return island;
let bindIndex = 0;
const attrs = node.attrs
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
@@ -2446,6 +2472,8 @@ ${exposed
${declarations}
${visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
const __response = __values.find((value) => value instanceof Response);
if (__response) return __response;
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: ""}
}`;
@@ -2765,7 +2793,7 @@ function renderClientControlTemplate(nodes) {
}
const componentTag = isComponentTag(node.tag);
let bindIndex = 0;
const attrs = node.attrs
const attrs = expandBindings(node.attrs)
.map((attribute) => {
const name = attribute.event
? componentTag
@@ -2803,19 +2831,21 @@ function encodeClientControl(value) {
}
function renderComponentIfNode(node, ctx) {
let expression = "``";
const clientBranches = [];
for (let index = node.branches.length - 1; index >= 0; index--) {
const branch = node.branches[index];
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
const bodyExpression = "`" + body + "`";
clientBranches.unshift(`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`);
expression =
branch.cond === null
? bodyExpression
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
}
const definition = encodeClientControl(node.branches.map((branch) => ({
cond: branch.cond,
body: renderClientControlTemplate(branch.body),
})));
// Render all branches on the server into an inert, encoded definition.
// A branch first selected in the browser therefore contains the real nested
// component HTML and scope metadata, not a data-component placeholder.
const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
}
function renderComponentEachNode(node, ctx) {
@@ -2955,7 +2985,7 @@ function renderComponentNode(node, ctx) {
}
}
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
const attrs = node.attrs
const attrs = expandBindings(node.attrs)
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
.map((a) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
@@ -3413,6 +3443,9 @@ function __wrnRaw(v: unknown): string {
}`);
}
if (needsScope) {
out.push(`function __wrnexusEncodeControl(value: unknown): string {
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) {
return "undefined";
@@ -3495,7 +3528,7 @@ function __wrnProp(v) {
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
return __wrnAttr(value);
}
function renderPageComponentAttr(attr, dynamicExpressions) {
function renderPageComponentAttr(attr, dynamicExpressions, _reactive) {
if (attr.event) {
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
}
@@ -3508,7 +3541,11 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
}
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
return ` ${attr.name}="${marker}"`;
// The mount disappears during SSR. Preserve every dynamic component prop
// as a parent-owned binding; server-only expressions simply fail closed in
// the browser, while page state can continue driving the mounted child.
const binding = ` data-wrn-prop-bind-${dynamicExpressions.length - 1}="${attrEscape(JSON.stringify([attr.name, attr.value]))}"`;
return ` ${attr.name}="${marker}"${binding}`;
}
},
@@ -4030,6 +4067,7 @@ function islandNamesFrom(imports) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.NativeCompileError = void 0;
exports.generateNative = generateNative;
const syntax_1 = require("@wrnexus/syntax");
class NativeCompileError extends Error {
constructor(message) {
super(message);
@@ -4095,12 +4133,12 @@ function expression(value) {
function textJsx(value) {
const pieces = [];
let last = 0;
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
if (match.index > last)
pieces.push(value.slice(last, match.index));
const expr = match[1].trim();
for (const segment of (0, syntax_1.extractInterpolations)(value)) {
if (segment.start > last)
pieces.push(value.slice(last, segment.start));
const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
last = match.index + match[0].length;
last = segment.end;
}
pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
@@ -6190,6 +6228,26 @@ function expandStructuredStateDeclarations(lines, unit) {
}
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
if (!match)
return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth)
return null;
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`),
];
}
catch {
return null;
}
}
function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100;
@@ -6245,6 +6303,12 @@ function formatWrnPass(source, options = {}) {
index += 1;
continue;
}
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value);
@@ -6325,7 +6389,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.extractInterpolations = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
var tokenizer_ts_1 = require("./tokenizer.js");
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
@@ -6337,6 +6401,8 @@ Object.defineProperty(exports, "parse", { enumerable: true, get: function () { r
Object.defineProperty(exports, "parseHtmlView", { enumerable: true, get: function () { return parser_ts_1.parseHtmlView; } });
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return parser_ts_1.ParseError; } });
Object.defineProperty(exports, "VOID_ELEMENTS", { enumerable: true, get: function () { return parser_ts_1.VOID_ELEMENTS; } });
var interpolation_ts_1 = require("./interpolation.js");
Object.defineProperty(exports, "extractInterpolations", { enumerable: true, get: function () { return interpolation_ts_1.extractInterpolations; } });
var types_ts_1 = require("./types.js");
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return types_ts_1.eraseFunctionTypes; } });
Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return types_ts_1.inferredRuntimeType; } });
@@ -6370,6 +6436,50 @@ Object.defineProperty(exports, "parseStoreLifecycle", { enumerable: true, get: f
Object.defineProperty(exports, "parseStructuredImports", { enumerable: true, get: function () { return v060_ts_1.parseStructuredImports; } });
Object.defineProperty(exports, "stripRuntimeFunctionModifiers", { enumerable: true, get: function () { return v060_ts_1.stripRuntimeFunctionModifiers; } });
},
"packages/syntax/src/interpolation.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractInterpolations = extractInterpolations;
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
function extractInterpolations(value) {
const segments = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote = null;
while (index < value.length && depth > 0) {
const char = value[index];
if (quote) {
if (char === "\\" && index + 1 < value.length)
index += 2;
else {
if (char === quote)
quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`")
quote = char;
else if (char === "{")
depth++;
else if (char === "}")
depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
},
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
"use strict";
File diff suppressed because it is too large Load Diff
+151 -14
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// WRN editor language server source hash: 3c6060ff9db332cf1dc01a467d795fb2d002300400072a4b87437c48d61ab7f9
// WRN editor language server source hash: 5258b3e76cec47df752d4bb886fbd57a3f3c4af9ca9b6b2bf6742c8d78025ff1
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
@@ -23,12 +23,14 @@ var __toESM = (mod, isNodeMode, target) => {
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: __accessProp.bind(mod, key),
enumerable: true
});
if (mod && typeof mod === "object" || typeof mod === "function") {
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: __accessProp.bind(mod, key),
enumerable: true
});
}
if (canCache)
cache.set(mod, to);
return to;
@@ -66,7 +68,7 @@ var __export = (target, all) => {
};
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js
var require_typescript = __commonJS((exports2, module2) => {
var require_typescript = __commonJS(function(exports2, module2) {
var __dirname = "E:\\WireJS\\node_modules\\.bun\\typescript@6.0.3\\node_modules\\typescript\\lib", __filename = "E:\\WireJS\\node_modules\\.bun\\typescript@6.0.3\\node_modules\\typescript\\lib\\typescript.js";
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
@@ -169514,7 +169516,7 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
});
// node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js
var require_main = __commonJS((exports2, module2) => {
var require_main = __commonJS(function(exports2, module2) {
var __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames;
@@ -169658,9 +169660,9 @@ var require_main = __commonJS((exports2, module2) => {
// packages/language-server/src/server.ts
var exports_server = {};
__export(exports_server, {
isApiReferenceAt: () => isApiReferenceAt,
apiCallCompletions: () => apiCallCompletions,
apiCallHover: () => apiCallHover,
apiCallCompletions: () => apiCallCompletions
isApiReferenceAt: () => isApiReferenceAt
});
module.exports = __toCommonJS(exports_server);
@@ -170720,6 +170722,26 @@ function expandStructuredStateDeclarations(lines, unit) {
}
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
if (!match)
return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth)
return null;
const jsonLines = JSON.stringify(parsed, null, unit).split(`
`);
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`)
];
} catch {
return null;
}
}
function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100;
@@ -170776,6 +170798,12 @@ function formatWrnPass(source, options = {}) {
index += 1;
continue;
}
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value);
@@ -172391,6 +172419,44 @@ function parseHtmlView(src, pos) {
const nodes = parseNodeList("root");
return { nodes, endPos: i };
}
// packages/syntax/src/interpolation.ts
function extractInterpolations(value) {
const segments = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote = null;
while (index < value.length && depth > 0) {
const char = value[index];
if (quote) {
if (char === "\\" && index + 1 < value.length)
index += 2;
else {
if (char === quote)
quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`")
quote = char;
else if (char === "{")
depth++;
else if (char === "}")
depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
// packages/syntax/src/diagnostics.ts
function stripAsciiControlAndSpace(value) {
let result = "";
@@ -172988,7 +173054,7 @@ function runtimeNamespace(runtime) {
return `__wrn_${runtime}`;
}
function functionDeclaration(fn) {
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": Event & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
}
function retainedImports(ast) {
@@ -173048,6 +173114,27 @@ var RESERVED_BINDING_NAMES = new Set([
function safeBindingName(name) {
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
}
function safeViewExpression(expression, reservedProps) {
if (!reservedProps.size)
return expression;
const scanner = import_typescript.default.createScanner(import_typescript.default.ScriptTarget.Latest, false, import_typescript.default.LanguageVariant.Standard, expression);
let result = "";
let copied = 0;
for (let token = scanner.scan();token !== import_typescript.default.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
const start = scanner.getTokenPos();
const end = scanner.getTextPos();
const text = scanner.getTokenText();
if (!reservedProps.has(text))
continue;
const previous = expression.slice(0, start).trimEnd().at(-1);
const following = expression.slice(end).trimStart().at(0);
if (previous === "." || following === ":")
continue;
result += expression.slice(copied, start) + `props.${text}`;
copied = end;
}
return result + expression.slice(copied);
}
function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
const ast = parse(source);
const chunks = [];
@@ -173103,6 +173190,8 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
append(`declare const api: { ${apiType} };`);
append(`declare const props: Readonly<${ast.name}Props>;`);
append("declare const refs: Record<string, Element | null>;");
append("declare const toast: ((message: string, options?: Record<string, unknown>) => unknown) & { success(message: string, options?: Record<string, unknown>): unknown; error(message: string, options?: Record<string, unknown>): unknown; danger(message: string, options?: Record<string, unknown>): unknown; warning(message: string, options?: Record<string, unknown>): unknown; info(message: string, options?: Record<string, unknown>): unknown; dismiss(id: string | number): void; clear(): void };");
append("declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;");
append(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
append(importedWrnDeclarations(ast, filePath, appRoot));
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
@@ -173119,6 +173208,53 @@ ${sharedAliases}`);
append(functionDeclaration(fn), fn.source);
append("}");
}
const viewFunctions = new Map;
for (const fn of ast.runtimeFunctions) {
if (fn.runtime !== "server")
viewFunctions.set(fn.name, fn.runtime);
}
for (const [name, runtime] of viewFunctions) {
append(`declare const ${name}: typeof ${runtimeNamespace(runtime)}.${name};`);
}
let bindingIndex = 0;
const reservedProps = new Set(ast.props.map((prop) => prop.name).filter((name) => !safeBindingName(name)));
const appendViewBindings = (nodes, locals) => {
for (const node of nodes) {
if (node.type === "element") {
for (const attr of node.attrs) {
const expressions = attr.event ? [attr.value] : extractInterpolations(attr.value).map((segment) => segment.expression);
for (const expression of expressions) {
if (!expression.trim())
continue;
const declarations = [
...[...locals].map((name) => `declare const ${name}: any;`),
...attr.event ? [
"declare const payload: any;",
"declare const event: Event & { target: HTMLElement };"
] : []
].join(" ");
append(`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${safeViewExpression(expression, reservedProps)}); }`, attr.value);
}
}
appendViewBindings(node.children, locals);
} else if (node.type === "if") {
for (const branch of node.branches) {
if (branch.cond)
append(`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(branch.cond, reservedProps)}); }`, branch.cond);
appendViewBindings(branch.body, locals);
}
} else if (node.type === "each") {
append(`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(node.list, reservedProps)}); }`, node.list);
const nested = new Set(locals);
nested.add(node.item);
if (node.index)
nested.add(node.index);
appendViewBindings(node.body, nested);
appendViewBindings(node.empty, locals);
}
}
};
appendViewBindings(ast.view, new Set);
return {
ast,
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
@@ -173224,7 +173360,7 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
const shape = shapes.get(node.tag);
if (!shape)
return;
const attributes = new Map(node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name, attr]));
const attributes = new Map(node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name, attr]));
const outputNames = new Set(shape.outputs.map((output) => output.name));
const position = lineAt(source, `<${node.tag}`);
for (const prop of shape.props) {
@@ -173261,7 +173397,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
}
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
continue;
const prop = known.get(attr.name);
const propName = attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name;
const prop = known.get(propName);
if (!prop) {
diagnostics.push({
code: "WRN-COMPONENT-UNKNOWN-PROP",
+14
View File
@@ -20,6 +20,20 @@ test("preserves nested prop defaults while formatting", () => {
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
});
test("formats large typed JSON prop defaults with the bundled formatter", () => {
const source = `component Catalog {
props {
categories: unknown[] = [{"label":"Platform & Core","apps":[{"label":"Home","href":"/home"},{"label":"SSO","href":"/sso"}]},{"label":"Sales","apps":[{"label":"CRM","href":"/crm"}]}]
}
view { <div></div> }
}`;
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2, printWidth: 70 });
assert.match(formatted, /categories: unknown\[\] = \[\n \{/);
assert.match(formatted, /"apps": \[\n \{/);
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2, printWidth: 70 }), formatted);
});
test("formats native JSON state values with readable indentation", () => {
const source = `component Footer {
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
+2 -2
View File
@@ -55,7 +55,7 @@
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
"sbom": "node scripts/generate-sbom.mjs",
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write",
"check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples",
"check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples && bun run test:services && bun run test:editor",
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
"stage:packages": "bun run scripts/publish-packages.ts",
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
@@ -70,7 +70,7 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/bun": "^1.3.14",
"@types/bun": "^1.4.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"eslint": "^10.8.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/auth",
"version": "0.8.17",
"version": "0.8.18",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+5
View File
@@ -1672,6 +1672,11 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
(typeof metadata.deviceId === "string" ? metadata.deviceId : undefined),
trusted: Boolean(rememberedDevice) || input.session?.trusted === true,
fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined,
metadata: {
...(input.session?.metadata ?? {}),
mfaVerifiedAt: now(),
mfaMethod: input.method,
},
});
user.lastLoginAt = now();
user.updatedAt = now();
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/authz",
"version": "0.8.14",
"version": "0.8.15",
"private": true,
"type": "module",
"main": "src/index.ts",
+5
View File
@@ -16,6 +16,11 @@ export function defineAuthz<Subject = any, Resource = any>(
const bindings = module.bindings ?? {};
for (const id of Object.keys(permissions)) {
if (id.startsWith("role:")) {
throw new Error(
`WRN-AUTHZ-DECL: permission id '${id}' uses the reserved 'role:' prefix; role grants use 'role:<name>' for inheritance.`,
);
}
if (id.includes("*")) {
throw new Error(
`WRN-AUTHZ-DECL: permission id '${id}' must not contain a wildcard; wildcards belong in roles.`,
+6
View File
@@ -26,6 +26,12 @@ describe("defineAuthz", () => {
expect(() => defineAuthz({ permissions: { "post:*": {} } })).toThrow(/wildcard/i);
});
test("rejects permission ids that collide with role inheritance", () => {
expect(() => defineAuthz({ permissions: { "role:assign": {} } })).toThrow(
/reserved 'role:' prefix/i,
);
});
test("rejects a role granting an unknown-shaped entry", () => {
expect(() => defineAuthz({ roles: { editor: [""] } })).toThrow(/role 'editor'/i);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/captcha",
"version": "0.8.11",
"version": "0.8.14",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module",
"sideEffects": false,
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test";
import {
captchaContext,
captchaResultResponse,
captchaTokenFrom,
verifyCaptchaOrThrow,
} from "../src/helpers.ts";
describe("CAPTCHA helper boundaries", () => {
test("extracts JSON request fallbacks without consuming the caller's request", async () => {
const request = new Request("https://example.test/verify", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ captchaToken: 12345 }),
});
expect(await captchaTokenFrom(request)).toBe("12345");
expect(await request.json()).toEqual({ captchaToken: 12345 });
});
test("turns verification failures into stable errors and no-store responses", async () => {
const provider = {
name: "custom" as const,
client: { responseField: "captchaToken" },
verify: async () => ({
success: false as const,
code: "expired",
message: "Try again",
provider: "custom" as const,
action: "login",
}),
};
await expect(
verifyCaptchaOrThrow(provider, { providerToken: "x", action: "login" }),
).rejects.toThrow("WRN-CAPTCHA-EXPIRED: Try again");
const response = captchaResultResponse({
success: false,
code: "invalid",
provider: "custom",
action: "login",
});
expect(response.status).toBe(403);
expect(response.headers.get("cache-control")).toBe("no-store");
expect(
captchaContext({
locals: { captcha: { success: true, provider: "custom", action: "login" } },
} as never),
).toEqual({ success: true, provider: "custom", action: "login" });
expect(captchaContext({ locals: { captcha: "forged" } } as never)).toBeNull();
});
test("does not convert provider transport failures into successful verification", async () => {
const provider = {
name: "custom" as const,
client: { responseField: "captchaToken" },
verify: async () => {
throw new Error("provider timeout");
},
};
await expect(
verifyCaptchaOrThrow(provider, { providerToken: "x", action: "checkout" }),
).rejects.toThrow("provider timeout");
});
});
+3 -2
View File
@@ -1,11 +1,12 @@
{
"name": "@wrnexus/cli",
"version": "0.8.57",
"version": "0.8.64",
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./workspace": "./src/workspace.ts"
"./workspace": "./src/workspace.ts",
"./test-setup": "./src/test-setup.ts"
},
"bin": {
"wrnexus": "src/index.ts"
+5
View File
@@ -113,6 +113,11 @@ Update options: --dry-run previews changes; --no-verify skips post-update check/
async function main(): Promise<void> {
const [command, ...rest] = process.argv.slice(2);
if (command === "--version" || command === "-v" || command === "version") {
console.log(currentCliVersion());
return;
}
notifyIfUpdateAvailable(currentCliVersion(), command);
switch (command) {
+14
View File
@@ -0,0 +1,14 @@
import { registerDb, setDb } from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { loadAppConfig, loadEnv } from "@wrnexus/styles";
const root = process.env.WRNEXUS_TEST_ROOT;
if (root) {
const profile = process.env.WRNEXUS_PROFILE || "test";
loadEnv(root, profile);
const config = await loadAppConfig(root, profile);
if (config.db) setDb(connectFromConfig(config.db, root));
for (const [name, database] of Object.entries(config.databases ?? {})) {
registerDb(name, connectFromConfig(database, root));
}
}
+21 -2
View File
@@ -20,6 +20,7 @@ export interface TestCommandPlan {
level?: TestLevel;
files: string[];
setup?: { command: string; args: string[] };
env?: Record<string, string>;
}
function shardFiles(files: string[], value?: string): string[] {
@@ -69,6 +70,11 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
const root = resolve(appRoot);
const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
const watch = args.includes("--watch");
const profile = args.find((value) => value.startsWith("--profile="))?.slice(10) || "test";
const setupSource = join(import.meta.dir, "test-setup.ts");
const setupModule = existsSync(setupSource)
? setupSource
: join(import.meta.dir, "test-setup.js");
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
.split(",")
@@ -120,10 +126,18 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
);
return {
command: process.execPath,
args: ["test", ...(watch ? ["--watch"] : []), ...(pattern ? files : []), ...passthrough],
args: [
"test",
"--preload",
setupModule,
...(watch ? ["--watch"] : []),
...(pattern ? files : []),
...passthrough,
],
cwd: root,
level,
files,
env: { WRNEXUS_TEST_ROOT: root, WRNEXUS_PROFILE: profile },
};
}
@@ -136,7 +150,12 @@ export function runTests(appRoot: string, args: string[]): ChildProcess | null {
process.exitCode = 1;
return null;
}
const launch = () => spawn(plan.command, plan.args, { stdio: "inherit", cwd: plan.cwd });
const launch = () =>
spawn(plan.command, plan.args, {
stdio: "inherit",
cwd: plan.cwd,
env: { ...process.env, ...plan.env },
});
if (plan.setup) {
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
setup.on("exit", (code, signal) => {
+13 -4
View File
@@ -63,14 +63,16 @@ const execFileAsync = promisify(execFile);
/** Resolve every installed WRNexus dependency independently and in parallel. */
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> {
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
const unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
const entries = await Promise.all(
unique.map(async (name) => {
try {
const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], {
encoding: "utf8",
});
const command = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "npm";
const args =
process.platform === "win32"
? ["/d", "/s", "/c", "npm", "view", name, "version", "--json"]
: ["view", name, "version", "--json"];
const { stdout } = await execFileAsync(command, args, { encoding: "utf8" });
const parsed = JSON.parse(stdout.trim()) as string | string[];
const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)
@@ -1274,6 +1276,13 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
? new Map<string, string>()
: await latestPackageVersions(dependencyNames);
if (!versionArg && dependencyNames.length > 0 && packageVersions.size === 0) {
throw new Error(
`Unable to resolve any of ${new Set(dependencyNames).size} installed WRNexus package ` +
`version(s) from npm. Check the @wrnexus registry and authentication before retrying.`,
);
}
console.log(`\n ⚡ wrnexus update → migrations ${target}${dryRun ? " (dry run)" : ""}`);
if (!versionArg) {
console.log(
+10 -1
View File
@@ -50,6 +50,15 @@ describe("test command planning", () => {
test("keeps the unfiltered legacy command", async () => {
const plan = createTestPlan(await fixture(), ["--watch"]);
expect(plan.level).toBeUndefined();
expect(plan.args).toEqual(["test", "--watch"]);
expect(plan.args[0]).toBe("test");
expect(plan.args).toContain("--preload");
expect(plan.args.at(-1)).toBe("--watch");
expect(plan.env?.WRNEXUS_PROFILE).toBe("test");
});
test("passes the selected profile to automatic database setup", async () => {
const plan = createTestPlan(await fixture(), ["--profile=ci"]);
expect(plan.env?.WRNEXUS_PROFILE).toBe("ci");
expect(plan.args).not.toContain("--profile=ci");
});
});
+24 -2
View File
@@ -1,8 +1,12 @@
import { afterEach, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { compareVersions, notifyIfUpdateAvailable } from "../src/update-notifier.ts";
import { join, resolve } from "node:path";
import {
compareVersions,
currentCliVersion,
notifyIfUpdateAvailable,
} from "../src/update-notifier.ts";
const originalCache = process.env.WRNEXUS_UPDATE_CACHE;
@@ -35,3 +39,21 @@ test("cached update checks suggest the exact upgrade command", () => {
rmSync(root, { recursive: true, force: true });
}
});
test("--version prints the package version outside a WrNexus workspace", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-version-"));
try {
const result = Bun.spawnSync({
cmd: [process.execPath, resolve(import.meta.dir, "../src/index.ts"), "--version"],
cwd: root,
env: { ...process.env, WRNEXUS_NO_UPDATE_CHECK: "1" },
stdout: "pipe",
stderr: "pipe",
});
expect(result.exitCode).toBe(0);
expect(result.stdout.toString().trim()).toBe(currentCliVersion());
expect(result.stderr.toString()).toBe("");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/compiler",
"version": "0.8.19",
"version": "0.8.22",
"type": "module",
"main": "src/index.ts",
"exports": {
+15 -3
View File
@@ -834,7 +834,7 @@ function renderPageComponentInvocation(
const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component")
.map((attr) => renderPageComponentAttr(attr, loops))
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
.join("");
const inner = node.children
@@ -1857,6 +1857,8 @@ ${declarations}
${
visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
const __response = __values.find((value) => value instanceof Response);
if (__response) return __response;
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: ""
}
@@ -3170,7 +3172,11 @@ function __wrnProp(v: unknown): string {
return __wrnAttr(value);
}
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string {
function renderPageComponentAttr(
attr: Attr,
dynamicExpressions: string[],
_reactive: PageReactive | null,
): string {
if (attr.event) {
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
}
@@ -3188,6 +3194,12 @@ function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): stri
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
// The mount disappears during SSR. Preserve every dynamic component prop
// as a parent-owned binding; server-only expressions simply fail closed in
// the browser, while page state can continue driving the mounted child.
const binding = ` data-wrn-prop-bind-${dynamicExpressions.length - 1}="${attrEscape(
JSON.stringify([attr.name, attr.value]),
)}"`;
return ` ${attr.name}="${marker}"`;
return ` ${attr.name}="${marker}"${binding}`;
}
+5 -5
View File
@@ -1,4 +1,4 @@
import type { Attr, PageAst, ViewNode } from "@wrnexus/syntax";
import { extractInterpolations, type Attr, type PageAst, type ViewNode } from "@wrnexus/syntax";
export class NativeCompileError extends Error {
constructor(message: string) {
@@ -68,11 +68,11 @@ function expression(value: string): string | null {
function textJsx(value: string): string {
const pieces: string[] = [];
let last = 0;
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
if (match.index! > last) pieces.push(value.slice(last, match.index));
const expr = match[1]!.trim();
for (const segment of extractInterpolations(value)) {
if (segment.start > last) pieces.push(value.slice(last, segment.start));
const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
last = match.index! + match[0].length;
last = segment.end;
}
pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
@@ -56,6 +56,17 @@ test("named loads support memoized dependencies and deferred execution", () => {
expect(code).toContain('return { "audit": __values[0] }');
});
test("named server loads propagate redirect responses instead of wrapping them as data", () => {
const code = generate(
parse(`page Private {
load server gate { return Response.redirect("/login", 303) }
view { <p>Private</p> }
}`),
);
expect(code).toContain("__values.find((value) => value instanceof Response)");
expect(code).toContain("if (__response) return __response");
});
test("load dependency cycles and cross-phase server dependencies fail compilation", () => {
expect(() =>
parse(
+10
View File
@@ -1151,6 +1151,16 @@ page Home {
expect(output).toContain("__wrnexusPropAttr([");
});
test("page state passed to a component retains a parent-owned prop binding", () => {
const output = generate(
parse(`page Home {
state count = 1
view { <Child value={count} /> }
}`),
);
expect(output).toContain("data-wrn-prop-bind-");
expect(output).toContain("{count}");
});
test("nested component props retain parent-owned reactive bindings", () => {
const output = generate(
parse(`component Parent {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/csr",
"version": "0.8.31",
"version": "0.8.35",
"type": "module",
"main": "src/index.ts",
"exports": {
+16 -1
View File
@@ -19,6 +19,21 @@ export { ACTION_RUNTIME } from "./action-runtime.ts";
export { createApiClient } from "./api-client.ts";
export type { ApiClientOptions, ApiRequest } from "./api-client.ts";
/** Public client transport used by `.wrn` client functions for dynamic URLs. */
export function useFetch<T = unknown>(
path: string,
methodOrOptions?:
string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown },
input?: unknown,
): Promise<T> {
const client = (
globalThis as typeof globalThis & { useFetch?: (...args: unknown[]) => Promise<T> }
).useFetch;
if (!client)
return Promise.reject(new Error("WRN-CSR: useFetch is only available in the browser runtime"));
return client(path, methodOrOptions, input);
}
const CONTROLLER_SECTIONS = ["PRIMARY", "UI", "PIN"] as const;
function runtimeForMode(development: boolean): string {
@@ -75,7 +90,7 @@ const CONTROLLER_LOADER = String.raw`
if (!componentControllerPromise) {
componentControllerPromise = new Promise(function (resolve, reject) {
var script = document.createElement("script");
script.src = componentControllerUrl;
script.src = trustedScriptURL(componentControllerUrl);
script.defer = true;
script.onload = resolve;
script.onerror = reject;
+63 -10
View File
@@ -26,6 +26,40 @@ export const REACTIVE_RUNTIME = String.raw`
var behaviorObserver;
var clientModuleCache = new Map();
/*
* Production enables require-trusted-types-for 'script'. Every HTML write
* below uses compiler-emitted template markup (or the explicit data-html
* escape hatch), and controller URLs are generated by the framework. Route
* those known sinks through the policy allowed by the default security
* preset instead of letting the browser reject hydration midway through a
* state update.
*/
var wrnexusTrustedTypesPolicy = null;
if (window.trustedTypes && typeof window.trustedTypes.createPolicy === "function") {
try {
wrnexusTrustedTypesPolicy = window.trustedTypes.createPolicy("wrnexus", {
createHTML: function (value) { return value; },
createScriptURL: function (value) { return value; }
});
} catch (_) {
wrnexusTrustedTypesPolicy = window.trustedTypes.defaultPolicy || null;
}
}
function trustedHTML(value) {
var source = value == null ? "" : String(value);
return wrnexusTrustedTypesPolicy
? wrnexusTrustedTypesPolicy.createHTML(source)
: source;
}
function trustedScriptURL(value) {
var source = value == null ? "" : String(value);
return wrnexusTrustedTypesPolicy
? wrnexusTrustedTypesPolicy.createScriptURL(source)
: source;
}
/*
* Two builtin chains the runtime reaches for constantly. Aliasing them is
* not only shorter: hasOwn keeps prototype keys from reading as data, and
@@ -234,8 +268,21 @@ export const REACTIVE_RUNTIME = String.raw`
Array.prototype.forEach.call(rules || [], function (rule) {
var cssText = rule.cssText || "";
var match;
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)/g;
while ((match = pattern.exec(cssText))) referenced[match[1]] = true;
// Only inspect declarations that can affect the current document. A
// component stylesheet may contain optional variants that are not
// rendered on this page, and variables with fallbacks are intentionally
// optional rather than missing theme tokens.
if (rule.selectorText) {
try {
if (!document.querySelector(rule.selectorText)) return;
} catch (_) {
// Keep inspecting selectors that the DOM implementation cannot parse.
}
}
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)\s*(,)?/g;
while ((match = pattern.exec(cssText))) {
if (!match[2]) referenced[match[1]] = true;
}
if (rule.style) {
Array.prototype.forEach.call(rule.style, function (property) {
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
@@ -1818,7 +1865,7 @@ export const REACTIVE_RUNTIME = String.raw`
var next = markup == null ? "" : String(markup);
if (htmlNode.innerHTML !== next) {
htmlNode.innerHTML = next;
htmlNode.innerHTML = trustedHTML(next);
}
});
runHtml();
@@ -2002,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
}
function clearControlContent() {
if (!rangeEnd) { block.innerHTML = ""; return; }
if (!rangeEnd) { block.innerHTML = trustedHTML(""); return; }
while (block.nextSibling && block.nextSibling !== rangeEnd) {
block.parentNode.removeChild(block.nextSibling);
}
@@ -2010,7 +2057,7 @@ export const REACTIVE_RUNTIME = String.raw`
function appendControlContent(markup, locals) {
var template = document.createElement("template");
template.innerHTML = markup || "";
template.innerHTML = trustedHTML(markup || "");
var fragment = template.content;
var elements = toArray(fragment.childNodes).filter(function (node) {
return node.nodeType === 1;
@@ -2399,7 +2446,7 @@ export const REACTIVE_RUNTIME = String.raw`
var markup = value == null ? "" : String(value);
if (node.innerHTML !== markup) {
node.innerHTML = markup;
node.innerHTML = trustedHTML(markup);
}
});
});
@@ -2775,9 +2822,15 @@ export const REACTIVE_RUNTIME = String.raw`
// server forwards these markers onto the rendered child root; evaluate
// them here and write changes into the child's prop signals.
toArray(el.querySelectorAll("*"))
.filter(isScopeRoot)
.filter(function (node) {
return toArray(node.attributes).some(function (attr) {
return attr.name.indexOf("data-wrn-prop-bind-") === 0;
});
})
.forEach(function (node) {
if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
var targetScope = isScopeRoot(node) ? node : ownerScope(node);
if (!targetScope || targetScope === el) return;
if (!targetScope.parentNode || ownerScope(targetScope.parentNode) !== el) return;
toArray(node.attributes).forEach(function (attr) {
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
var binding = pairBinding(attr.value);
@@ -2796,9 +2849,9 @@ export const REACTIVE_RUNTIME = String.raw`
});
} catch (_) { return; }
var apply = function () {
if (node.__wrnexusScopeApi) node.__wrnexusScopeApi.set(propName, value);
if (targetScope.__wrnexusScopeApi) targetScope.__wrnexusScopeApi.set(propName, value);
};
if (node.__wrnexusScopeApi) apply();
if (targetScope.__wrnexusScopeApi) apply();
else queueMicrotask(apply);
});
});
+55
View File
@@ -62,6 +62,13 @@ test("split runtime hydrates a controller only from the controller asset", () =>
expect(win.document.activeElement).toBe(buttons[1]);
});
test("production hydration uses Trusted Types for HTML and controller URLs", () => {
const runtime = getReactiveRuntime();
expect(runtime).toContain('createPolicy("wrnexus"');
expect(runtime).toContain('template.innerHTML = trustedHTML(markup || "")');
expect(runtime).toContain("script.src = trustedScriptURL(componentControllerUrl)");
});
test("a page without controller markers does not request the controller asset", () => {
const win = mount(
`<main data-scope="count: 1"><span>{count}</span></main>`,
@@ -454,6 +461,22 @@ test("parent state updates a mounted child's reactive prop", async () => {
expect(win.document.querySelector("#child-value")?.textContent).toBe("2");
});
test("a prop marker forwarded onto a child control still updates the child scope", async () => {
const win = mount(
`<div data-scope="enabled: false">` +
`<button data-on-click="enabled = true">enable</button>` +
`<div data-scope="disabled: true">` +
`<button id="child" disabled data-wrn-bind-0='["disabled","{disabled}"]' ` +
`data-wrn-prop-bind-0='["disabled","{!enabled}"]'>child</button>` +
`</div></div>`,
);
await Promise.resolve();
const buttons = win.document.querySelectorAll("button");
expect((buttons[1] as unknown as HTMLButtonElement).disabled).toBe(true);
(buttons[0] as unknown as HTMLElement).click();
expect((buttons[1] as unknown as HTMLButtonElement).disabled).toBe(false);
});
test("independent signals in one scope update correctly (dependency tracking)", () => {
const win = mount(
`<div data-scope="a: 0, b: 100">
@@ -1065,6 +1088,38 @@ test("development runtime accepts component-local and inline wrn variables", ()
expect(warnings).toHaveLength(0);
});
test("development runtime ignores fallback and inactive component variables", () => {
const win = new Window() as unknown as Window & Record<string, unknown>;
win.document.head.innerHTML =
`<style>.visible { color: var(--wrn-optional, red); } ` +
`.not-rendered { color: var(--wrn-inactive); }</style>`;
win.document.body.innerHTML = `<div class="visible" data-scope=""></div>`;
(globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location;
(globalThis as Record<string, unknown>).NodeFilter = (
win as unknown as { NodeFilter: unknown }
).NodeFilter;
(globalThis as Record<string, unknown>).MutationObserver = (
win as unknown as { MutationObserver: unknown }
).MutationObserver;
(globalThis as Record<string, unknown>).CustomEvent = (
win as unknown as { CustomEvent: unknown }
).CustomEvent;
const warnings: unknown[][] = [];
const originalWarn = console.warn;
console.warn = (...args: unknown[]) => warnings.push(args);
try {
(0, eval)(getReactiveRuntime(true));
(
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
).__wrnexusHydrateScopes?.(win.document);
} finally {
console.warn = originalWarn;
}
expect(warnings).toHaveLength(0);
});
test("production runtime strips development diagnostics", () => {
const production = getReactiveRuntime();
expect(production).not.toContain("WRN-DEV-");
+11 -3
View File
@@ -15,9 +15,17 @@ function fakeDb() {
test("seed helpers parameterize object rows and removals", async () => {
const { db, calls } = fakeDb();
await addSeedData(db, [{ code: "free", credits: 10 }, { code: "pro", credits: 20 }], "plans", {
conflict: "ignore",
});
await addSeedData(
db,
[
{ code: "free", credits: 10 },
{ code: "pro", credits: 20 },
],
"plans",
{
conflict: "ignore",
},
);
await removeSeedData(db, { code: "free" }, "plans");
expect(calls[0]).toEqual({
sql: "INSERT OR IGNORE INTO plans (code, credits) VALUES (?, ?)",
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.50",
"version": "0.8.56",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -25,6 +25,7 @@
"@wrnexus/uploader": "workspace:*",
"@wrnexus/plugin": "workspace:*",
"@wrnexus/queue": "workspace:*",
"@wrnexus/react": "workspace:*",
"@wrnexus/store": "workspace:*",
"@wrnexus/security": "workspace:*",
"@wrnexus/observability": "workspace:*",
+6
View File
@@ -367,6 +367,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
externalRoutes: pluginContributions.routes,
middlewareFiles: pluginContributions.middleware,
});
// UI and plugin components are intentionally registered application-wide;
// using them without a source import is not an implicit-import diagnostic.
setCompileImportOptions(appRoot, {
...importConfig,
globalComponents: router.components.map((component) => component.name),
});
const styleEntry = opts.styleEntry ?? null;
const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar);
+18 -3
View File
@@ -86,6 +86,7 @@ interface CompileImportOptions {
mode: ImportMode;
aliases: Record<string, string>;
autoImport: boolean;
globalComponents: Set<string>;
}
const compileImportOptions = new Map<string, CompileImportOptions>();
const warnedImportDiagnostics = new Set<string>();
@@ -104,12 +105,18 @@ export function setDevCompilerPipeline(pipeline: DevCompilerPipeline | null): vo
export function setCompileImportOptions(
appRoot: string,
options: { mode?: ImportMode; aliases?: Record<string, string>; autoImport?: boolean } = {},
options: {
mode?: ImportMode;
aliases?: Record<string, string>;
autoImport?: boolean;
globalComponents?: Iterable<string>;
} = {},
): void {
compileImportOptions.set(resolve(appRoot), {
mode: options.mode ?? "compatible",
aliases: { "@": "./app", ...(options.aliases ?? {}) },
autoImport: options.autoImport ?? true,
globalComponents: new Set(options.globalComponents ?? []),
});
}
@@ -141,11 +148,18 @@ function importOptionsHash(file: string): string {
mode: "compatible" as const,
aliases: { "@": "./app" },
autoImport: true,
globalComponents: new Set<string>(),
};
const aliases = Object.fromEntries(
Object.entries(options.aliases).sort(([left], [right]) => left.localeCompare(right)),
);
return hashPath(JSON.stringify({ ...options, aliases }));
return hashPath(
JSON.stringify({
...options,
aliases,
globalComponents: [...options.globalComponents].sort(),
}),
);
}
/** Island bundles already built this dev session, keyed by resolved source. */
@@ -493,7 +507,8 @@ function validateConfiguredImports(_source: string, ast: PageAst, file: string):
"Transition",
]);
const missing = [...usedComponents].filter(
(name) => !compilerBuiltins.has(name) && !imported.has(name),
(name) =>
!compilerBuiltins.has(name) && !options.globalComponents.has(name) && !imported.has(name),
);
if (ast.layoutIsSymbol && ast.layout && !imported.has(ast.layout)) missing.push(ast.layout);
if (!missing.length) return;
+30 -2
View File
@@ -1739,6 +1739,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// Props may carry `{t:key}` i18n markers — resolve them for the active
// language before handing them to the component.
const props = resolveTProps(parseComponentProps(attrStr!), translate);
// Client control templates retain reactive expressions (for example
// `metrics="{metrics}"`) because their parent state is not available
// while we pre-render the inert branch metadata. Do not feed those
// expression strings into typed component coercion. Rendering with the
// component defaults creates the real child boundary; the bridged prop
// binding synchronizes the live value when the branch is instantiated.
for (const [key, value] of Object.entries(props)) {
if (/^\{[\s\S]*\}$/.test(value) && !/^\{t:/.test(value)) delete props[key];
}
if (normalizedName === "languageswitcher" && deps.i18n) {
props.locales ??= JSON.stringify(
deps.i18n.langs.map((locale) => ({
@@ -1769,7 +1778,20 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
},
)
: renderComponent();
result += await renderComponents(rendered, translate, language, depth + 1);
// Prop bindings are authored by the parent mount but consumed by the
// rendered child's scope. The mount itself is replaced during SSR, so
// carry those reserved attributes onto the child's scope root.
const propBindings = Array.from(
attrStr!.matchAll(/\s(data-wrn-prop-bind-[A-Za-z0-9_-]+(?:="[^"]*")?)/g),
(match) => ` ${match[1]}`,
).join("");
const bridged = propBindings
? rendered.replace(
/(<[A-Za-z][A-Za-z0-9-]*\b[^>]*\bdata-scope="[^"]*")/,
`$1${propBindings}`,
)
: rendered;
result += await renderComponents(bridged, translate, language, depth + 1);
} catch (err) {
console.error(`[wrnexus] component '${name}' failed to render`, err);
deps.devToolbar?.collector.add(
@@ -1950,6 +1972,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
).getOrLoad(`route:${matched.route.raw}`, () => load(pageCtx));
}
(pageCtx as Context & { data?: unknown }).data = data;
if (data instanceof Response) return data;
if (data && typeof data === "object") Object.assign(pageCtx, data);
}
let body = await renderComponents(String(await component(pageCtx)), ctx.t, ctx.lang);
@@ -2668,7 +2691,12 @@ export function parseComponentProps(attrStr: string): Record<string, string> {
const props: Record<string, string> = {};
for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)(?:="([^"]*)")?/g)) {
const key = m[1]!;
if (key === "data-component") continue;
if (
key === "data-component" ||
key.startsWith("data-wrn-bind-") ||
key.startsWith("data-wrn-prop-bind-")
)
continue;
props[key] = decodeHtmlEntities(m[2] ?? "");
}
return props;
@@ -90,6 +90,92 @@ test("a component in an initially hidden client branch is server-prepared for in
expect(branches[0].body).not.toContain("data-component=");
});
test("typed components in client branches use defaults until reactive props are available", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-reactive-branch-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
mkdirSync(join(app, "components"), { recursive: true });
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
writeFileSync(join(app, "components/Panel.wrn"), "component Panel { view { <section /> } }\n");
const marker = "[&quot;items&quot;,&quot;{items}&quot;]";
const definition = Buffer.from(
JSON.stringify([
{
cond: "open",
body: `<div data-component="Panel" items="{items}" visible="{true}" data-wrn-prop-bind-0="${marker}"></div>`,
},
]),
"utf8",
).toString("base64");
let observed: Record<string, string> | undefined;
const handlers = createHandlers({
mode: "development",
hmr: false,
router: buildRouter(app),
loadModule: async (file) =>
basename(file) === "Panel.wrn"
? {
render: (props: Record<string, string>) => {
observed = props;
return '<section data-scope="items: [], visible: false" data-wrn-scope="e30="></section>';
},
}
: {
default: () =>
`<template data-wrn-if="${definition}"></template><template data-wrn-control-end></template>`,
},
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/"), {
upgrade: () => false,
});
const html = await response!.text();
expect(observed).toEqual({});
const encoded = /data-wrn-if="([A-Za-z0-9+/=]+)"/.exec(html)?.[1];
const branches = JSON.parse(Buffer.from(encoded!, "base64").toString("utf8"));
expect(branches[0].body).toContain("<section");
expect(branches[0].body).toContain("data-wrn-prop-bind-0");
expect(branches[0].body).not.toContain("data-component=");
});
test("SSR carries a parent prop binding onto the rendered child scope", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-prop-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
mkdirSync(join(app, "components"), { recursive: true });
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
writeFileSync(
join(app, "components/Child.wrn"),
"component Child { props { value = 0 } view { <b>{value}</b> } }\n",
);
const marker = "[&quot;value&quot;,&quot;{count}&quot;]";
const handlers = createHandlers({
mode: "development",
hmr: false,
router: buildRouter(app),
loadModule: async (file) =>
basename(file) === "Child.wrn"
? { render: () => '<div data-scope="value: 1" data-wrn-scope="e30="><b>1</b></div>' }
: {
default: () =>
`<div data-component="Child" value="1" data-wrn-prop-bind-0="${marker}"></div>`,
},
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/"), {
upgrade: () => false,
});
const html = await response!.text();
expect(html).toContain(`data-wrn-prop-bind-0="${marker}"`);
expect(html).not.toContain('data-component="Child"');
});
test("translated text is present in the raw server response", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-ssr-i18n-"));
roots.push(root);
@@ -71,6 +71,13 @@ test("compiler-native reactive elements do not require application imports", ()
expect(() => compileWrnArtifacts(page, 5)).not.toThrow();
});
test("globally registered plugin components do not require source imports", () => {
const { root, page } = fixture();
writeFileSync(page, "page Home { view { <SignIn /> } }");
setCompileImportOptions(root, { mode: "explicit", globalComponents: ["SignIn"] });
expect(() => compileWrnArtifacts(page, 6)).not.toThrow();
});
test("component examples in comments do not require imports", () => {
const { root, page } = fixture();
writeFileSync(
@@ -32,6 +32,32 @@ test("server loader data is available to page rendering", async () => {
expect(await response!.text()).toContain("Ada,Lin / 2");
});
test("a server loader can return a redirect Response", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-load-redirect-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/private.ts"), "export default () => '';\n");
const handlers = createHandlers({
mode: "production",
hmr: false,
router: buildRouter(app),
loadModule: async () => ({
__wrnexusLoad: async () => Response.redirect("https://example.test/login", 303),
default: () => {
throw new Error("redirected pages must not render");
},
}),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/private"), {
upgrade: () => false,
});
expect(response?.status).toBe(303);
expect(response?.headers.get("location")).toBe("https://example.test/login");
});
test("HMR page synchronization initializes request-scoped loader caching", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-load-runtime-"));
roots.push(root);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-toolbar",
"version": "0.8.13",
"version": "0.8.16",
"private": true,
"type": "module",
"sideEffects": false,
@@ -0,0 +1,41 @@
import { expect, test } from "bun:test";
import { createServerIssue, issueFromError } from "../../src/server/issues.ts";
test("server issues have stable fingerprints and retain actionable error context", () => {
const input = {
ruleId: "server/query",
category: "server" as const,
severity: "warning" as const,
title: "Slow query",
message: "Query exceeded budget",
pathname: "/orders",
source: { file: "app/api/orders.ts", line: 12, column: 4 },
};
const first = createServerIssue(input);
const second = createServerIssue(input);
expect(first.id).not.toBe(second.id);
expect(first.fingerprint).toBe(second.fingerprint);
expect(first.metadata?.pathname).toBe("/orders");
const error = issueFromError(new Error("database offline"), { pathname: "/orders" });
expect(error.severity).toBe("error");
expect(error.message).toBe("database offline");
expect(error.metadata?.stack).toContain("database offline");
expect(error.recommendation).toContain("stack trace");
});
test("non-Error throws are normalized without losing caller context", () => {
const issue = issueFromError(
{ reason: "offline" },
{
ruleId: "server/dependency",
title: "Dependency failed",
pathname: "/health",
source: { file: "app/api/health.ts", line: 4 },
},
);
expect(issue.ruleId).toBe("server/dependency");
expect(issue.title).toBe("Dependency failed");
expect(issue.message).toBe("[object Object]");
expect(issue.fingerprint).toContain("app/api/health.ts:4:0");
});
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/mail",
"version": "0.8.1",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -8,6 +8,7 @@
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/encryption": "workspace:*",
"@wrnexus/queue": "workspace:*"
}
}
+114 -5
View File
@@ -1,4 +1,5 @@
import type { DefinedJob } from "@wrnexus/queue";
import { createKeyring, open, seal, type EncryptionKeyring } from "@wrnexus/encryption";
export interface MailMessage {
from?: string;
@@ -14,6 +15,41 @@ export interface MailDriver {
test?(): Promise<{ ok: boolean; error?: string }>;
}
export interface SealedMailDriverOptions {
/** Ciphertext stored by the application; plaintext is never retained here. */
sealedCredential: string;
unseal(value: string): Promise<string>;
create(credential: string): MailDriver | Promise<MailDriver>;
/** Cache the initialized driver. Defaults to true. */
cache?: boolean;
}
/**
* Lazily opens a sealed credential only at the transport boundary. This keeps
* encryption policy and SMTP/vendor choice independent while preventing every
* app from rebuilding the same decrypt-on-send lifecycle.
*/
export function sealedMailDriver(options: SealedMailDriverOptions): MailDriver {
let cached: Promise<MailDriver> | undefined;
const create = async () => options.create(await options.unseal(options.sealedCredential));
const driver = () =>
options.cache === false
? create()
: (cached ??= create().catch((error) => {
cached = undefined;
throw error;
}));
return {
async send(message) {
return (await driver()).send(message);
},
async test() {
const resolved = await driver();
return resolved.test?.() ?? { ok: true };
},
};
}
export interface MailTemplate<T = Record<string, unknown>> {
subject(data: T): string;
html?(data: T): string;
@@ -26,14 +62,87 @@ export interface MailOptions {
sandbox?: { enabled?: boolean; allowlist?: readonly string[] };
}
export function defineMail(options: MailOptions) {
export interface MailSandboxDecision {
allowed: boolean;
reason?: string;
}
/** Fail-closed, case-insensitive sandbox policy shared by mail workers and defineMail. */
export function mailSandboxDecision(
sandbox: { enabled?: boolean | number; allowlist?: readonly string[] | string },
recipient: unknown,
): MailSandboxDecision {
if (!sandbox.enabled) return { allowed: true };
if (typeof recipient !== "string" || !recipient.trim()) {
return {
allowed: false,
reason: "sandbox mode is on and no valid recipient address was given",
};
}
let source: unknown = sandbox.allowlist ?? [];
if (typeof source === "string") {
try {
source = JSON.parse(source || "[]");
} catch {
source = [];
}
}
const allowed = new Set(
(options.sandbox?.allowlist ?? []).map((value) => value.trim().toLowerCase()),
(Array.isArray(source) ? source : [])
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim().toLowerCase())
.filter(Boolean),
);
const normalized = recipient.trim().toLowerCase();
return allowed.has(normalized)
? { allowed: true }
: {
allowed: false,
reason: `sandbox mode is on and ${recipient} is not in the sandbox allow-list`,
};
}
export interface SealedMailCredentialsOptions {
/** Environment variable holding a base64 AES key. */
env?: string;
keyId?: string;
}
/** Owns validated, lazy encryption-key setup for credentials stored at rest. */
export function defineSealedMailCredentials(options: SealedMailCredentialsOptions = {}) {
const env = options.env ?? "APP_ENCRYPTION_KEY";
let cached: EncryptionKeyring | undefined;
const keyring = () => {
if (cached) return cached;
const secret = process.env[env];
if (!secret)
throw new Error(`${env} is not set; it is required to seal mail credentials at rest.`);
let length: number;
try {
length = atob(secret).length;
} catch {
throw new Error(`${env} must be a valid base64-encoded 16, 24 or 32 byte key.`);
}
if (![16, 24, 32].includes(length)) {
throw new Error(
`${env} must decode to 16, 24 or 32 bytes; the current value decodes to ${length}.`,
);
}
return (cached = createKeyring([{ id: options.keyId ?? "primary", secret, active: true }]));
};
return {
keyring,
seal: (plain: string) => seal(plain, keyring()),
open: (sealed: string) => open(sealed, keyring()),
};
}
export function defineMail(options: MailOptions) {
const assertAllowed = (recipients: readonly string[]) => {
if (!options.sandbox?.enabled) return;
const denied = recipients.find((recipient) => !allowed.has(recipient.trim().toLowerCase()));
if (denied) throw new Error(`WRN-MAIL-SANDBOX: recipient '${denied}' is not allow-listed`);
for (const recipient of recipients) {
const decision = mailSandboxDecision(options.sandbox ?? {}, recipient);
if (!decision.allowed) throw new Error(`WRN-MAIL-SANDBOX: ${decision.reason}`);
}
};
return {
async send(message: MailMessage) {
+56 -1
View File
@@ -1,5 +1,11 @@
import { expect, test } from "bun:test";
import { defineMail, defineMailTemplate } from "../src/index.ts";
import {
defineMail,
defineMailTemplate,
defineSealedMailCredentials,
mailSandboxDecision,
sealedMailDriver,
} from "../src/index.ts";
test("mail templates render and sandbox blocks accidental recipients", async () => {
const sent: unknown[] = [];
@@ -16,3 +22,52 @@ test("mail templates render and sandbox blocks accidental recipients", async ()
expect(sent).toHaveLength(1);
await expect(mail.send({ to: "real@example.com", subject: "no" })).rejects.toThrow("SANDBOX");
});
test("mail owns fail-closed sandbox decisions and sealed credential setup", async () => {
expect(
mailSandboxDecision({ enabled: 1, allowlist: '["Test@Example.com"]' }, "test@example.com"),
).toEqual({ allowed: true });
expect(
mailSandboxDecision({ enabled: true, allowlist: "not-json" }, "x@example.com").allowed,
).toBe(false);
const previous = process.env.TEST_MAIL_ENCRYPTION_KEY;
process.env.TEST_MAIL_ENCRYPTION_KEY = btoa(String.fromCharCode(...new Uint8Array(32).fill(7)));
try {
const credentials = defineSealedMailCredentials({ env: "TEST_MAIL_ENCRYPTION_KEY" });
const sealed = await credentials.seal("smtp-secret");
expect(sealed).not.toContain("smtp-secret");
expect(await credentials.open(sealed)).toBe("smtp-secret");
} finally {
if (previous === undefined) delete process.env.TEST_MAIL_ENCRYPTION_KEY;
else process.env.TEST_MAIL_ENCRYPTION_KEY = previous;
}
});
test("sealed drivers decrypt lazily once and delegate send/test", async () => {
let opens = 0;
const sent: string[] = [];
const driver = sealedMailDriver({
sealedCredential: "ciphertext",
async unseal(value) {
opens++;
expect(value).toBe("ciphertext");
return "plaintext-secret";
},
create(credential) {
expect(credential).toBe("plaintext-secret");
return {
async send(message) {
sent.push(message.subject);
},
async test() {
return { ok: true };
},
};
},
});
expect(opens).toBe(0);
await driver.send({ to: "safe@example.test", subject: "one" });
expect(await driver.test?.()).toEqual({ ok: true });
expect(opens).toBe(1);
expect(sent).toEqual(["one"]);
});
+40
View File
@@ -0,0 +1,40 @@
# @wrnexus/metering
Framework-native entitlement catalogs, credit packs, and usage metering for WrNexus applications.
The package keeps policy separate from persistence: applications provide a `MeterStore`, while the metering facade validates unit amounts and reports insufficient balances separately from storage faults. All amounts are positive safe integers except explicit adjustments, which may be positive or negative but never zero.
## Usage
```ts
import { defineEntitlements, defineMeter, definePacks } from "@wrnexus/metering";
const entitlements = defineEntitlements({
plans: () => [
{ code: "free", name: "Free", features: [], allowance: 10 },
{ code: "pro", name: "Pro", features: ["exports"], allowance: 1_000 },
],
subscriptionFor: async (subjectId) => subscriptions.planFor(subjectId),
fallback: "free",
});
const packs = definePacks([
{ code: "starter", units: 100 },
{ code: "plus", units: 500 },
]);
const meter = defineMeter({ store });
if (await entitlements.enabled("user-1", "exports")) {
const result = await meter.reserve("user-1", 1, "export report");
if (!result.ok) console.error(result.reason);
}
```
## API
- `defineEntitlements(options)` resolves a subject's plan, features, and allowance from a server-owned catalog, with an explicit fallback plan.
- `definePacks(entries)` creates an immutable, prototype-safe lookup of purchasable unit packs. Clients select a pack code rather than supplying an amount.
- `defineMeter(options)` exposes `balance`, `reserve`, `grant`, `purchase`, `refund`, and `adjust` operations over an application-provided store.
Meter operations return `{ ok: true }` on success or `{ ok: false, reason, fault? }` on refusal. A result with `fault: true` identifies an underlying storage error; use `onFault` for operational reporting without exposing exception-based control flow to callers.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/metering",
"version": "0.8.2",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
+14 -2
View File
@@ -90,6 +90,10 @@ export function defineMeter(options: MeterOptions) {
Number.isSafeInteger(units) && units > 0
? null
: { ok: false, reason: `${label} must be a positive whole number` };
const validSigned = (units: number): MeterResult | null =>
Number.isSafeInteger(units) && units !== 0
? null
: { ok: false, reason: `${label} adjustment must be a non-zero whole number` };
const fault = (error: unknown, operation: string): MeterResult => {
options.onFault?.(error, operation);
return {
@@ -138,7 +142,15 @@ export function defineMeter(options: MeterOptions) {
add("purchase", subjectId, units, reason, reference),
refund: (subjectId: string, units: number, reason: string, reference = "") =>
add("refund", subjectId, units, reason, reference),
adjust: (subjectId: string, units: number, reason: string, reference = "") =>
add("adjust", subjectId, units, reason, reference),
async adjust(subjectId: string, units: number, reason: string, reference = "") {
const invalid = validSigned(units);
if (invalid) return invalid;
try {
await options.store.write(subjectId, units, "adjust", reason, reference);
return { ok: true } as const;
} catch (error) {
return fault(error, "adjust");
}
},
};
}
+24
View File
@@ -40,3 +40,27 @@ test("meter distinguishes refusals from storage faults", async () => {
fault: true,
});
});
test("adjust accepts signed non-zero amounts while other additions stay positive", async () => {
const writes: number[] = [];
const meter = defineMeter({
store: {
balance: async () => 0,
reserve: async () => true,
write: async (_subject, units) => {
writes.push(units);
},
},
});
expect(await meter.adjust("u1", -25, "correction")).toEqual({ ok: true });
expect(await meter.adjust("u1", 10, "correction")).toEqual({ ok: true });
expect(await meter.adjust("u1", 0, "correction")).toEqual({
ok: false,
reason: "units adjustment must be a non-zero whole number",
});
expect(await meter.grant("u1", -1, "invalid")).toEqual({
ok: false,
reason: "units must be a positive whole number",
});
expect(writes).toEqual([-25, 10]);
});
+7
View File
@@ -0,0 +1,7 @@
# @wrnexus/payment
Gateway-neutral, webhook-authoritative payments for WrNexus. Amounts are integer minor units. Every initialize and refund operation requires an idempotency key. The package never accepts or stores card numbers, CVVs, or expiry input; its components navigate to provider-hosted checkout.
Built-in adapters: offline `sandbox`, Stripe, Razorpay, and PayPal. Applications may add adapters with `defineGateway`. Capabilities are explicit, unsupported operations are refused, signed webhooks are mandatory in production, and package-owned migrations provision the append-only event ledger.
Webhook events are deduplicated by the provider's event ID and reduced in provider occurrence/sequence order. Captured and refund states are monotonic, so a delayed retry of an older event cannot regress a terminal payment.
+8
View File
@@ -0,0 +1,8 @@
component PayNow {
props { checkoutUrl: string label?: string = "Pay now" disabled?: boolean = false }
view {
<a class="wrn-payment-button" href={checkoutUrl} aria-disabled={disabled}>
{label}
</a>
}
}
@@ -0,0 +1,4 @@
component PaymentHistory {
props { payments: array = [] }
view { <ol class="wrn-payment-history">{#each payments as payment}<li>{payment.currency} {payment.amount} — {payment.status}</li>{/each}</ol> }
}
@@ -0,0 +1,4 @@
component PaymentMethodList {
props { methods: array = [] }
view { <ul class="wrn-payment-methods">{#each methods as method}<li>{method.brand} ending {method.last4}</li>{/each}</ul> }
}
@@ -0,0 +1,4 @@
component PaymentSheet {
props { checkoutUrl: string gateway: string }
view { <section class="wrn-payment-sheet" data-gateway={gateway}><p>Secure payment is completed on the payment provider.</p><a href={checkoutUrl}>Continue securely</a></section> }
}
@@ -0,0 +1,4 @@
component PaymentStatus {
props { status: string label?: string = "Payment status" }
view { <div class="wrn-payment-status" role="status"><strong>{label}</strong><span>{status}</span></div> }
}
@@ -0,0 +1,4 @@
component PaymentSummary {
props { captured: number = 0 refunded: number = 0 net: number = 0 currency: string }
view { <dl><dt>Captured</dt><dd>{currency} {captured}</dd><dt>Refunded</dt><dd>{currency} {refunded}</dd><dt>Net</dt><dd>{currency} {net}</dd></dl> }
}
@@ -0,0 +1,4 @@
component RefundButton {
props { paymentId: string refundable: number partial?: boolean = false action?: string = "/api/payments/refund" }
view { <form method="post" action={action}><input type="hidden" name="paymentId" value={paymentId} /><label>Reason<input name="reason" required /></label>{#if partial}<label>Amount<input name="amount" type="number" min="1" max={refundable} /></label>{/if}<button type="submit">Refund</button></form> }
}
+22
View File
@@ -0,0 +1,22 @@
{
"name": "@wrnexus/payment",
"version": "0.8.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"files": [
"src",
"components",
"README.md"
],
"exports": {
".": "./src/index.ts",
"./adapters": "./src/adapters.ts",
"./plugin": "./src/plugin.ts",
"./runtime-webhook": "./src/runtime-webhook.ts"
},
"dependencies": {
"@wrnexus/db": "workspace:*",
"@wrnexus/plugin": "workspace:*"
}
}
+556
View File
@@ -0,0 +1,556 @@
import { defineGateway } from "./core.ts";
import type {
GatewayCapabilities,
GatewayIntent,
GatewayRefund,
PaymentGateway,
PaymentStatus,
} from "./types.ts";
const encoder = new TextEncoder();
const hex = (bytes: ArrayBuffer) =>
[...new Uint8Array(bytes)].map((value) => value.toString(16).padStart(2, "0")).join("");
async function hmac(body: string, secret: string) {
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
return hex(await crypto.subtle.sign("HMAC", key, encoder.encode(body)));
}
function equal(left: string, right: string) {
if (left.length !== right.length) return false;
let difference = 0;
for (let i = 0; i < left.length; i++) difference |= left.charCodeAt(i) ^ right.charCodeAt(i);
return difference === 0;
}
const CAPABILITIES: GatewayCapabilities = {
authorizeThenCapture: false,
partialRefund: true,
multipleRefunds: true,
storedMethods: false,
customerVault: false,
hostedFields: false,
hostedCheckout: true,
webhookSignature: true,
payouts: false,
disputes: false,
};
const statusOf = (value: string): PaymentStatus =>
(({
succeeded: "captured",
captured: "captured",
paid: "captured",
authorized: "authorized",
requires_capture: "authorized",
cancelled: "cancelled",
canceled: "cancelled",
failed: "failed",
refunded: "refunded",
})[value] as PaymentStatus | undefined) ?? "pending";
export interface SandboxOptions {
secret?: string;
failure?: "create" | "refund" | "capture";
authorize?: boolean;
}
export function sandboxGateway(options: SandboxOptions = {}): PaymentGateway {
const intents = new Map<string, GatewayIntent>();
const refunds = new Map<string, GatewayRefund>();
return defineGateway({
id: "sandbox",
capabilities: {
...CAPABILITIES,
authorizeThenCapture: true,
storedMethods: true,
customerVault: true,
hostedFields: true,
},
supports: { currencies: "any", countries: "any" },
async createIntent(input) {
if (options.failure === "create") throw new Error("sandbox create failure");
const value = {
ref: `sb_${input.id}`,
status: (options.authorize || input.capture === "manual"
? "authorized"
: "pending") as PaymentStatus,
amount: input.amount,
currency: input.currency,
checkoutUrl: `https://sandbox.wrnexus.test/pay/${input.id}`,
clientSecret: `sandbox_${input.id}`,
};
intents.set(value.ref, value);
return value;
},
async fetchIntent(ref) {
const value = intents.get(ref);
if (!value) throw new Error("sandbox intent not found");
return structuredClone(value);
},
async cancelIntent(ref) {
const value = await this.fetchIntent(ref);
const next = { ...value, status: "cancelled" as const };
intents.set(ref, next);
return next;
},
async capture(ref, amount) {
if (options.failure === "capture") throw new Error("sandbox capture failure");
const value = await this.fetchIntent(ref);
const next = {
...value,
status: "captured" as const,
amount: amount?.amount ?? value.amount,
};
intents.set(ref, next);
return next;
},
async refund(input) {
if (options.failure === "refund") throw new Error("sandbox refund failure");
const existing = refunds.get(input.idempotencyKey);
if (existing) return existing;
const value = {
ref: `sbr_${crypto.randomUUID()}`,
status: "succeeded" as const,
amount: input.amount,
};
refunds.set(input.idempotencyKey, value);
return value;
},
async verifyWebhook(req, expected) {
const body = await req.text();
const signature = req.headers.get("x-wrnexus-signature") ?? "";
if (!equal(signature, await hmac(body, expected)))
throw new Error("invalid sandbox signature");
const payload = JSON.parse(body);
const intent = intents.get(payload.intentRef);
if (intent) intents.set(payload.intentRef, { ...intent, status: payload.status });
return {
id: String(payload.id),
intentRef: String(payload.intentRef),
type: String(payload.type ?? "payment.updated"),
status: payload.status,
occurredAt: Number(payload.occurredAt ?? Date.now()),
sequence: payload.sequence,
payload,
};
},
async listMethods() {
return [];
},
async attachMethod(_customer, token) {
return { ref: `sbm_${token}`, brand: "sandbox", last4: token.slice(-4) };
},
async detachMethod() {},
async createCustomer(input) {
return `sb_customer_${input.subject}`;
},
clientConfig(intent) {
return {
gateway: "sandbox",
clientSecret: intent.clientSecret ?? "",
checkoutUrl: intent.checkoutUrl ?? "",
};
},
});
}
export async function signSandboxWebhook(payload: unknown, secret = "sandbox-secret") {
const body = JSON.stringify(payload);
return { body, signature: await hmac(body, secret) };
}
async function jsonRequest(url: string, init: RequestInit) {
const response = await fetch(url, init);
const data = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`payment gateway request failed (${response.status})`);
return data as any;
}
export interface StripeConfig {
secretKey: string;
publishableKey: string;
webhookSecret: string;
apiVersion?: string;
}
export function stripeGateway(config: StripeConfig): PaymentGateway<StripeConfig> {
const auth = {
authorization: `Bearer ${config.secretKey}`,
"content-type": "application/x-www-form-urlencoded",
"stripe-version": config.apiVersion ?? "2026-03-31",
};
const form = (data: Record<string, unknown>) =>
new URLSearchParams(
Object.entries(data)
.filter(([, v]) => v !== undefined)
.map(([k, v]) => [k, String(v)]),
);
return defineGateway({
id: "stripe",
config,
capabilities: {
...CAPABILITIES,
authorizeThenCapture: true,
storedMethods: true,
customerVault: true,
hostedFields: true,
disputes: true,
},
supports: { currencies: "any", countries: "any" },
async createIntent(input) {
const row = await jsonRequest("https://api.stripe.com/v1/payment_intents", {
method: "POST",
headers: { ...auth, "idempotency-key": input.idempotencyKey },
body: form({
amount: input.amount,
currency: input.currency.toLowerCase(),
capture_method: input.capture === "manual" ? "manual" : "automatic",
"metadata[wrnexus_id]": input.id,
}),
});
return {
ref: row.id,
status: statusOf(row.status),
amount: row.amount,
currency: String(row.currency).toUpperCase(),
clientSecret: row.client_secret,
raw: row,
};
},
async fetchIntent(ref) {
const row = await jsonRequest(
`https://api.stripe.com/v1/payment_intents/${encodeURIComponent(ref)}`,
{ headers: auth },
);
return {
ref: row.id,
status: statusOf(row.status),
amount: row.amount,
currency: String(row.currency).toUpperCase(),
raw: row,
};
},
async cancelIntent(ref, reason) {
const row = await jsonRequest(
`https://api.stripe.com/v1/payment_intents/${encodeURIComponent(ref)}/cancel`,
{ method: "POST", headers: auth, body: form({ cancellation_reason: reason }) },
);
return {
ref: row.id,
status: "cancelled",
amount: row.amount,
currency: String(row.currency).toUpperCase(),
raw: row,
};
},
async capture(ref, amount) {
const row = await jsonRequest(
`https://api.stripe.com/v1/payment_intents/${encodeURIComponent(ref)}/capture`,
{ method: "POST", headers: auth, body: form({ amount_to_capture: amount?.amount }) },
);
return {
ref: row.id,
status: statusOf(row.status),
amount: row.amount_received,
currency: String(row.currency).toUpperCase(),
raw: row,
};
},
async refund(input) {
const row = await jsonRequest("https://api.stripe.com/v1/refunds", {
method: "POST",
headers: { ...auth, "idempotency-key": input.idempotencyKey },
body: form({
payment_intent: input.intentRef,
amount: input.amount,
reason: "requested_by_customer",
}),
});
return {
ref: row.id,
status: row.status === "succeeded" ? "succeeded" : "pending",
amount: row.amount,
raw: row,
};
},
async verifyWebhook(req, secret) {
const body = await req.text();
const header = req.headers.get("stripe-signature") ?? "";
const timestamp = /t=([^,]+)/.exec(header)?.[1] ?? "";
const signature = /v1=([^,]+)/.exec(header)?.[1] ?? "";
if (!timestamp || !equal(signature, await hmac(`${timestamp}.${body}`, secret)))
throw new Error("invalid Stripe signature");
const row = JSON.parse(body),
object = row.data.object;
return {
id: row.id,
intentRef: object.payment_intent ?? object.id,
type: row.type,
status: statusOf(object.status),
occurredAt: Number(row.created ?? timestamp) * 1000,
payload: row,
};
},
async listMethods(customerRef) {
const row = await jsonRequest(
`https://api.stripe.com/v1/payment_methods?customer=${encodeURIComponent(customerRef)}&type=card`,
{ headers: auth },
);
return (row.data ?? []).map((method: any) => ({
ref: method.id,
brand: method.card?.brand,
last4: method.card?.last4,
expires: method.card ? `${method.card.exp_month}/${method.card.exp_year}` : undefined,
}));
},
async attachMethod(customerRef, token) {
const row = await jsonRequest(
`https://api.stripe.com/v1/payment_methods/${encodeURIComponent(token)}/attach`,
{ method: "POST", headers: auth, body: form({ customer: customerRef }) },
);
return {
ref: row.id,
brand: row.card?.brand,
last4: row.card?.last4,
expires: row.card ? `${row.card.exp_month}/${row.card.exp_year}` : undefined,
};
},
async detachMethod(methodRef) {
await jsonRequest(
`https://api.stripe.com/v1/payment_methods/${encodeURIComponent(methodRef)}/detach`,
{ method: "POST", headers: auth },
);
},
async createCustomer(input) {
const row = await jsonRequest("https://api.stripe.com/v1/customers", {
method: "POST",
headers: auth,
body: form({ email: input.email, name: input.name, "metadata[subject]": input.subject }),
});
return row.id;
},
clientConfig(intent) {
return {
gateway: "stripe",
publishableKey: config.publishableKey,
clientSecret: intent.clientSecret ?? "",
};
},
});
}
export interface RazorpayConfig {
keyId: string;
keySecret: string;
webhookSecret: string;
theme?: { color?: string };
}
export function razorpayGateway(config: RazorpayConfig): PaymentGateway<RazorpayConfig> {
const auth = `Basic ${btoa(`${config.keyId}:${config.keySecret}`)}`;
const request = (path: string, init: RequestInit = {}) =>
jsonRequest(`https://api.razorpay.com/v1${path}`, {
...init,
headers: { authorization: auth, "content-type": "application/json", ...(init.headers ?? {}) },
});
return defineGateway({
id: "razorpay",
config,
capabilities: {
...CAPABILITIES,
authorizeThenCapture: false,
storedMethods: false,
customerVault: false,
},
supports: { currencies: ["INR"], countries: ["IN"] },
async createIntent(input) {
const row = await request("/orders", {
method: "POST",
body: JSON.stringify({
amount: input.amount,
currency: input.currency,
receipt: input.id,
notes: input.metadata,
}),
});
return {
ref: row.id,
status: "pending",
amount: row.amount,
currency: row.currency,
raw: row,
};
},
async fetchIntent(ref) {
const row = await request(`/orders/${encodeURIComponent(ref)}`);
return {
ref: row.id,
status: statusOf(row.status),
amount: row.amount,
currency: row.currency,
raw: row,
};
},
async cancelIntent(ref) {
const row = await this.fetchIntent(ref);
return { ...row, status: "cancelled" };
},
async refund(input) {
const row = await request(`/payments/${encodeURIComponent(input.intentRef)}/refund`, {
method: "POST",
headers: { "X-Razorpay-Idempotency": input.idempotencyKey },
body: JSON.stringify({ amount: input.amount, notes: { reason: input.reason } }),
});
return {
ref: row.id,
status: row.status === "processed" ? "succeeded" : "pending",
amount: row.amount,
raw: row,
};
},
async verifyWebhook(req, secret) {
const body = await req.text();
if (!equal(req.headers.get("x-razorpay-signature") ?? "", await hmac(body, secret)))
throw new Error("invalid Razorpay signature");
const row = JSON.parse(body),
payment = row.payload?.payment?.entity ?? row.payload?.order?.entity;
return {
id: row.id ?? `${row.event}:${payment.id}`,
intentRef: payment.order_id ?? payment.id,
type: row.event,
status: statusOf(payment.status),
occurredAt:
Number(payment.created_at ?? row.created_at ?? Math.floor(Date.now() / 1000)) * 1000,
payload: row,
};
},
clientConfig(intent) {
return {
gateway: "razorpay",
keyId: config.keyId,
orderId: intent.ref,
themeColor: config.theme?.color ?? "",
};
},
});
}
export interface PayPalConfig {
clientId: string;
clientSecret: string;
webhookId: string;
environment?: "sandbox" | "live";
}
export function paypalGateway(config: PayPalConfig): PaymentGateway<PayPalConfig> {
const base =
config.environment === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
let token = "";
async function headers() {
if (!token) {
const row = await jsonRequest(`${base}/v1/oauth2/token`, {
method: "POST",
headers: {
authorization: `Basic ${btoa(`${config.clientId}:${config.clientSecret}`)}`,
"content-type": "application/x-www-form-urlencoded",
},
body: "grant_type=client_credentials",
});
token = row.access_token;
}
return { authorization: `Bearer ${token}`, "content-type": "application/json" };
}
const request = async (path: string, init: RequestInit = {}) =>
jsonRequest(`${base}${path}`, {
...init,
headers: { ...(await headers()), ...(init.headers ?? {}) },
});
return defineGateway({
id: "paypal",
config,
capabilities: { ...CAPABILITIES, partialRefund: true, multipleRefunds: true },
supports: { currencies: "any", countries: "any" },
async createIntent(input) {
const row = await request("/v2/checkout/orders", {
method: "POST",
headers: { "PayPal-Request-Id": input.idempotencyKey },
body: JSON.stringify({
intent: "CAPTURE",
purchase_units: [
{
reference_id: input.id,
amount: { currency_code: input.currency, value: (input.amount / 100).toFixed(2) },
},
],
application_context: { return_url: input.returnUrl },
}),
});
return {
ref: row.id,
status: statusOf(String(row.status).toLowerCase()),
amount: input.amount,
currency: input.currency,
checkoutUrl: row.links?.find((x: any) => x.rel === "approve")?.href,
raw: row,
};
},
async fetchIntent(ref) {
const row = await request(`/v2/checkout/orders/${encodeURIComponent(ref)}`);
const unit = row.purchase_units?.[0]?.amount;
return {
ref: row.id,
status: statusOf(String(row.status).toLowerCase()),
amount: Math.round(Number(unit?.value ?? 0) * 100),
currency: unit?.currency_code ?? "USD",
raw: row,
};
},
async cancelIntent(ref) {
const row = await this.fetchIntent(ref);
return { ...row, status: "cancelled" };
},
async refund(input) {
const row = await request(
`/v2/payments/captures/${encodeURIComponent(input.intentRef)}/refund`,
{
method: "POST",
headers: { "PayPal-Request-Id": input.idempotencyKey },
body: JSON.stringify({
amount: { value: (input.amount / 100).toFixed(2), currency_code: input.currency },
note_to_payer: input.reason,
}),
},
);
return {
ref: row.id,
status: String(row.status).toUpperCase() === "COMPLETED" ? "succeeded" : "pending",
amount: input.amount,
raw: row,
};
},
async verifyWebhook(req) {
const row = await req.json();
const verification = await request("/v1/notifications/verify-webhook-signature", {
method: "POST",
body: JSON.stringify({
auth_algo: req.headers.get("paypal-auth-algo"),
cert_url: req.headers.get("paypal-cert-url"),
transmission_id: req.headers.get("paypal-transmission-id"),
transmission_sig: req.headers.get("paypal-transmission-sig"),
transmission_time: req.headers.get("paypal-transmission-time"),
webhook_id: config.webhookId,
webhook_event: row,
}),
});
if (verification.verification_status !== "SUCCESS")
throw new Error("invalid PayPal signature");
return {
id: row.id,
intentRef: row.resource?.supplementary_data?.related_ids?.order_id ?? row.resource?.id,
type: row.event_type,
status: statusOf(String(row.resource?.status ?? "").toLowerCase()),
occurredAt: Date.parse(row.create_time ?? "") || Date.now(),
payload: row,
};
},
clientConfig(intent) {
return { gateway: "paypal", clientId: config.clientId, orderId: intent.ref };
},
});
}
+118
View File
@@ -0,0 +1,118 @@
import type { CreateIntentInput, GatewayIntent, PaymentGateway } from "./types.ts";
export interface GatewayContractReport {
ok: boolean;
issues: string[];
}
/** Structural half of the shared adapter suite, usable by third-party adapters in CI. */
export function verifyGatewayContract(adapter: PaymentGateway): GatewayContractReport {
const issues: string[] = [];
const capability = (name: string, declared: boolean, present: boolean) => {
if (declared !== present)
issues.push(
`${adapter.id}: capability '${name}' is ${declared ? "declared but not implemented" : "implemented but not declared"}`,
);
};
capability(
"authorizeThenCapture",
adapter.capabilities.authorizeThenCapture,
typeof adapter.capture === "function",
);
capability(
"storedMethods",
adapter.capabilities.storedMethods,
typeof adapter.listMethods === "function" &&
typeof adapter.attachMethod === "function" &&
typeof adapter.detachMethod === "function",
);
capability(
"customerVault",
adapter.capabilities.customerVault,
typeof adapter.createCustomer === "function",
);
if (!adapter.capabilities.hostedFields && !adapter.capabilities.hostedCheckout)
issues.push(`${adapter.id}: a hosted payment surface is required`);
if (!adapter.capabilities.webhookSignature)
issues.push(`${adapter.id}: signed webhooks are required`);
return { ok: issues.length === 0, issues };
}
export interface GatewayContractFixture {
input: CreateIntentInput;
webhook(intent: GatewayIntent): Request | Promise<Request>;
}
/** Executable lifecycle contract shared by built-in and third-party adapters. */
export async function exerciseGatewayContract(
adapter: PaymentGateway,
fixture: GatewayContractFixture,
): Promise<GatewayContractReport> {
const report = verifyGatewayContract(adapter);
const issues = [...report.issues];
const check = (condition: unknown, message: string) => {
if (!condition) issues.push(`${adapter.id}: ${message}`);
};
try {
const intent = await adapter.createIntent(fixture.input);
check(Boolean(intent.ref), "createIntent returned no reference");
check(intent.amount === fixture.input.amount, "createIntent changed the amount");
check(intent.currency === fixture.input.currency, "createIntent changed the currency");
const duplicate = await adapter.createIntent(fixture.input);
check(duplicate.ref === intent.ref, "createIntent did not honor its idempotency key");
const fetched = await adapter.fetchIntent(intent.ref);
check(fetched.ref === intent.ref, "fetchIntent returned a different reference");
const client = adapter.clientConfig(intent);
const configuredSecrets = Object.entries((adapter.config ?? {}) as Record<string, unknown>)
.filter(([key, value]) => /secret|password/i.test(key) && typeof value === "string")
.map(([, value]) => value);
check(
!configuredSecrets.some((secret) => Object.values(client).includes(secret as string)),
"clientConfig exposed a configured secret",
);
if (adapter.capabilities.authorizeThenCapture) {
const captured = await adapter.capture!(intent.ref);
check(Boolean(captured.ref), "capture returned no reference");
}
const cancellable = await adapter.createIntent({
...fixture.input,
id: `${fixture.input.id}-cancel`,
idempotencyKey: `${fixture.input.idempotencyKey}:cancel`,
});
const cancelled = await adapter.cancelIntent(cancellable.ref, "contract");
check(cancelled.status === "cancelled", "cancelIntent did not return cancelled state");
const refund = await adapter.refund({
intentRef: intent.ref,
amount: Math.max(1, Math.floor(intent.amount / 2)),
currency: intent.currency,
reason: "contract",
idempotencyKey: `${fixture.input.idempotencyKey}:refund`,
});
check(Boolean(refund.ref), "refund returned no reference");
if (adapter.capabilities.customerVault) {
const customer = await adapter.createCustomer!({ subject: fixture.input.subject });
check(Boolean(customer), "createCustomer returned no reference");
}
if (adapter.capabilities.storedMethods) {
const customer = "contract-customer";
const method = await adapter.attachMethod!(customer, "contract-token-4242");
check(Boolean(method.ref), "attachMethod returned no reference");
check(
Array.isArray(await adapter.listMethods!(customer)),
"listMethods did not return an array",
);
await adapter.detachMethod!(method.ref);
}
const event = await adapter.verifyWebhook(
await fixture.webhook(intent),
"contract-webhook-secret",
);
check(Boolean(event.id), "verified webhook has no gateway event id");
check(Boolean(event.intentRef), "verified webhook has no intent reference");
check(Number.isFinite(event.occurredAt), "verified webhook has no gateway occurrence time");
} catch (error) {
issues.push(
`${adapter.id}: lifecycle threw ${error instanceof Error ? error.message : String(error)}`,
);
}
return { ok: issues.length === 0, issues };
}
+435
View File
@@ -0,0 +1,435 @@
import type {
GatewayMethod,
Money,
PaymentEvent,
PaymentGateway,
PaymentIntent,
PaymentMethod,
PaymentRefund,
PaymentResult,
PaymentStatus,
PaymentStore,
WebhookEvent,
} from "./types.ts";
export interface InitializePaymentInput extends Money {
subject: string;
idempotencyKey: string;
gateway?: string;
country?: string;
capture?: "automatic" | "manual";
metadata?: Record<string, string>;
returnUrl?: string;
}
export interface PaymentOptions {
store: PaymentStore;
adapters: readonly PaymentGateway[];
default: string;
route?(input: InitializePaymentInput): string;
webhookSecrets?: Record<string, string>;
environment?: "development" | "test" | "production";
}
const validMoney = (money: Money) =>
Number.isSafeInteger(money.amount) && money.amount > 0 && /^[A-Z]{3}$/.test(money.currency);
const refusal = <T>(reason: string): PaymentResult<T> => ({ ok: false, fault: "refused", reason });
const gatewayFault = <T>(reason: string): PaymentResult<T> => ({
ok: false,
fault: "gateway",
reason,
});
export function derivePaymentStatus(events: readonly PaymentEvent[]): PaymentStatus {
const ordered = [...events].sort((left, right) => {
if (left.sequence !== undefined && right.sequence !== undefined) {
const numeric = Number(left.sequence) - Number(right.sequence);
if (Number.isFinite(numeric) && numeric !== 0) return numeric;
const lexical = String(left.sequence).localeCompare(String(right.sequence));
if (lexical) return lexical;
}
return (
(left.occurredAt ?? left.at) - (right.occurredAt ?? right.at) ||
left.id.localeCompare(right.id)
);
});
let status: PaymentStatus = "created";
for (const event of ordered) {
if (status === "refunded") continue;
if (
status === "partially_refunded" &&
!["partially_refunded", "refunded"].includes(event.status)
)
continue;
if (
status === "captured" &&
["created", "pending", "authorized", "failed", "cancelled"].includes(event.status)
)
continue;
if (status === "authorized" && ["created", "pending"].includes(event.status)) continue;
status = event.status;
}
return status;
}
export function defineGateway<T extends PaymentGateway>(gateway: T): T {
if (!gateway.id.trim()) throw new Error("WRN-PAYMENT-GATEWAY: id is required");
if (!gateway.capabilities.webhookSignature && process.env.NODE_ENV === "production")
throw new Error(`WRN-PAYMENT-WEBHOOK: gateway '${gateway.id}' cannot verify signatures`);
return Object.freeze(gateway);
}
export function definePayment(options: PaymentOptions) {
const adapters = new Map(options.adapters.map((adapter) => [adapter.id, adapter]));
if (adapters.size !== options.adapters.length)
throw new Error("WRN-PAYMENT-CONFIG: duplicate gateway id");
if (!adapters.has(options.default))
throw new Error(`WRN-PAYMENT-CONFIG: default gateway '${options.default}' is not registered`);
if ((options.environment ?? process.env.NODE_ENV) === "production")
for (const adapter of adapters.values())
if (!adapter.capabilities.webhookSignature)
throw new Error(`WRN-PAYMENT-WEBHOOK: gateway '${adapter.id}' cannot run in production`);
const adapterFor = (id: string) => adapters.get(id);
const status = async (id: string) => derivePaymentStatus(await options.store.events(id));
async function initializePayment(
input: InitializePaymentInput,
): Promise<PaymentResult<PaymentIntent>> {
if (!validMoney(input))
return refusal("amount must be positive minor units and currency must be ISO-4217");
if (!input.subject.trim() || !input.idempotencyKey.trim())
return refusal("subject and idempotencyKey are required");
try {
const existing = await options.store.findIntentByKey(input.idempotencyKey);
if (existing) return { ok: true, value: existing };
const gatewayId = input.gateway ?? options.route?.(input) ?? options.default;
const adapter = adapterFor(gatewayId);
if (!adapter) return refusal(`gateway '${gatewayId}' is not configured`);
if (
adapter.supports.currencies !== "any" &&
!adapter.supports.currencies.includes(input.currency)
)
return refusal(`gateway '${gatewayId}' does not support ${input.currency}`);
if (
input.country &&
adapter.supports.countries !== "any" &&
!adapter.supports.countries.includes(input.country)
)
return refusal(`gateway '${gatewayId}' does not support country ${input.country}`);
if (input.capture === "manual" && !adapter.capabilities.authorizeThenCapture)
return refusal(`gateway '${gatewayId}' does not support authorize-then-capture`);
const id = `pay_${crypto.randomUUID()}`;
const remote = await adapter.createIntent({ ...input, id });
const intent: PaymentIntent = {
id,
subject: input.subject,
amount: input.amount,
currency: input.currency,
gateway: gatewayId,
gatewayRef: remote.ref,
idempotencyKey: input.idempotencyKey,
createdAt: Date.now(),
checkoutUrl: remote.checkoutUrl,
client: adapter.clientConfig(remote),
metadata: { ...(input.metadata ?? {}) },
};
await options.store.putIntent(intent);
await options.store.appendEvent({
id: `local:${id}:created`,
intentId: id,
type: "payment.created",
status: remote.status,
at: Date.now(),
});
return { ok: true, value: intent };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "gateway failed");
}
}
async function getPayment(id: string) {
return options.store.getIntent(id);
}
async function checkPayment(id: string): Promise<PaymentStatus> {
return status(id);
}
async function syncPayment(id: string): Promise<PaymentResult<PaymentIntent>> {
try {
const intent = await options.store.getIntent(id);
if (!intent) return refusal("payment not found");
const remote = await adapterFor(intent.gateway)!.fetchIntent(intent.gatewayRef);
await options.store.appendEvent({
id: `sync:${intent.gateway}:${remote.ref}:${remote.status}`,
intentId: id,
type: "payment.synced",
status: remote.status,
at: Date.now(),
payload: remote.raw,
});
return { ok: true, value: intent };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "sync failed");
}
}
const confirmPayment = syncPayment;
async function capturePayment(id: string, amount?: Money): Promise<PaymentResult<PaymentIntent>> {
try {
const intent = await options.store.getIntent(id);
if (!intent) return refusal("payment not found");
const adapter = adapterFor(intent.gateway)!;
if (!adapter.capabilities.authorizeThenCapture || !adapter.capture)
return refusal(`gateway '${adapter.id}' does not support capture`);
const remote = await adapter.capture(intent.gatewayRef, amount);
await options.store.appendEvent({
id: `capture:${remote.ref}:${Date.now()}`,
intentId: id,
type: "payment.captured",
status: remote.status,
at: Date.now(),
payload: remote.raw,
});
return { ok: true, value: intent };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "capture failed");
}
}
async function cancelPayment(id: string, reason: string): Promise<PaymentResult<PaymentIntent>> {
try {
const intent = await options.store.getIntent(id);
if (!intent) return refusal("payment not found");
if (!reason.trim()) return refusal("cancellation reason is required");
const remote = await adapterFor(intent.gateway)!.cancelIntent(intent.gatewayRef, reason);
await options.store.appendEvent({
id: `cancel:${remote.ref}:${Date.now()}`,
intentId: id,
type: "payment.cancelled",
status: remote.status,
at: Date.now(),
payload: remote.raw,
});
return { ok: true, value: intent };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "cancellation failed");
}
}
async function refundableAmount(id: string): Promise<Money> {
const intent = await options.store.getIntent(id);
if (!intent) return { amount: 0, currency: "XXX" };
const captured =
(await status(id)) === "captured" ||
(await status(id)) === "partially_refunded" ||
(await status(id)) === "refunded"
? intent.amount
: 0;
const refunded = (await options.store.refunds(id))
.filter((row) => row.status !== "failed")
.reduce((sum, row) => sum + row.amount, 0);
return { amount: Math.max(0, captured - refunded), currency: intent.currency };
}
async function refundPayment(input: {
id: string;
amount?: number;
reason: string;
idempotencyKey: string;
}): Promise<PaymentResult<PaymentRefund>> {
try {
const intent = await options.store.getIntent(input.id);
if (!intent) return refusal("payment not found");
if (!input.reason.trim() || !input.idempotencyKey.trim())
return refusal("refund reason and idempotencyKey are required");
const prior = (await options.store.refunds(input.id)).find(
(row) => row.idempotencyKey === input.idempotencyKey,
);
if (prior) return { ok: true, value: prior };
const available = await refundableAmount(input.id);
const amount = input.amount ?? available.amount;
if (!Number.isSafeInteger(amount) || amount <= 0 || amount > available.amount)
return refusal("refund exceeds the refundable amount");
const adapter = adapterFor(intent.gateway)!;
if (amount < intent.amount && !adapter.capabilities.partialRefund)
return refusal(`gateway '${adapter.id}' does not support partial refunds`);
if (
(await options.store.refunds(input.id)).some((row) => row.status !== "failed") &&
!adapter.capabilities.multipleRefunds
)
return refusal(`gateway '${adapter.id}' does not support multiple refunds`);
const refund: PaymentRefund = {
id: `refund_${crypto.randomUUID()}`,
intentId: intent.id,
gatewayRef: "",
amount,
currency: intent.currency,
reason: input.reason,
idempotencyKey: input.idempotencyKey,
status: "pending",
createdAt: Date.now(),
};
if (!(await options.store.reserveRefund(refund, intent.amount)))
return refusal("refund is duplicated or exceeds the captured amount");
const remote = await adapter.refund({
intentRef: intent.gatewayRef,
amount,
currency: intent.currency,
reason: input.reason,
idempotencyKey: input.idempotencyKey,
});
await options.store.updateRefund(refund.id, remote.status, remote.ref);
const completed = { ...refund, status: remote.status, gatewayRef: remote.ref };
const remaining = available.amount - amount;
await options.store.appendEvent({
id: `refund:${remote.ref}`,
intentId: intent.id,
type: "payment.refunded",
status: remaining === 0 ? "refunded" : "partially_refunded",
at: Date.now(),
payload: remote.raw,
});
return { ok: true, value: completed };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "refund failed");
}
}
async function acceptWebhook(gatewayId: string, event: WebhookEvent): Promise<boolean> {
const intent = (await options.store.listIntents({ gateway: gatewayId })).find(
(row) => row.gatewayRef === event.intentRef,
);
if (!intent) return false;
return options.store.appendEvent({
id: `${gatewayId}:${event.id}`,
intentId: intent.id,
type: event.type,
status: event.status,
at: Date.now(),
occurredAt: event.occurredAt,
sequence: event.sequence,
payload: event.payload,
});
}
function paymentWebhookHandler(gatewayId: string) {
return async (ctx: { req: Request }) => {
const adapter = adapterFor(gatewayId);
const secret = options.webhookSecrets?.[gatewayId];
if (!adapter || !secret)
return Response.json({ error: "gateway webhook is not configured" }, { status: 404 });
try {
const event = await adapter.verifyWebhook(ctx.req, secret);
const accepted = await acceptWebhook(gatewayId, event);
return Response.json({ received: true, duplicate: !accepted });
} catch {
return Response.json({ error: "invalid webhook" }, { status: 400 });
}
};
}
async function paymentMethods(subject: string): Promise<PaymentMethod[]> {
return options.store.methods(subject);
}
async function attachPaymentMethod(
subject: string,
gatewayId: string,
customerRef: string,
token: string,
): Promise<PaymentResult<PaymentMethod>> {
try {
const adapter = adapterFor(gatewayId);
if (!adapter?.capabilities.storedMethods || !adapter.attachMethod)
return refusal(`gateway '${gatewayId}' does not support stored methods`);
const remote: GatewayMethod = await adapter.attachMethod(customerRef, token);
const method: PaymentMethod = {
id: `method_${crypto.randomUUID()}`,
subject,
gateway: gatewayId,
gatewayRef: remote.ref,
brand: remote.brand,
last4: remote.last4,
expires: remote.expires,
default: false,
};
await options.store.putMethod(method);
return { ok: true, value: method };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "method attach failed");
}
}
async function detachPaymentMethod(id: string): Promise<PaymentResult<void>> {
const method = await options.store.getMethod(id);
if (!method) return refusal("payment method not found");
const adapter = adapterFor(method.gateway);
try {
await adapter?.detachMethod?.(method.gatewayRef);
await options.store.removeMethod(id);
return { ok: true, value: undefined };
} catch (error) {
return gatewayFault(error instanceof Error ? error.message : "method detach failed");
}
}
async function setDefaultPaymentMethod(subject: string, id: string) {
await options.store.defaultMethod(subject, id);
}
async function listPayments(filter?: { subject?: string; gateway?: string }) {
return options.store.listIntents(filter);
}
async function listRefunds(id: string) {
return options.store.refunds(id);
}
async function paymentTotals(filter?: { subject?: string; gateway?: string }) {
const rows = await options.store.listIntents(filter);
const byCurrency: Record<string, { captured: number; refunded: number; net: number }> = {};
for (const row of rows) {
const state = await status(row.id);
const captured = ["captured", "partially_refunded", "refunded"].includes(state)
? row.amount
: 0;
const refunded = (await options.store.refunds(row.id))
.filter((x) => x.status === "succeeded")
.reduce((s, x) => s + x.amount, 0);
const total = (byCurrency[row.currency] ??= { captured: 0, refunded: 0, net: 0 });
total.captured += captured;
total.refunded += refunded;
total.net = total.captured - total.refunded;
}
return { byCurrency };
}
async function reconcilePayments() {
const discrepancies: unknown[] = [];
for (const intent of await options.store.listIntents()) {
const local = await status(intent.id);
const remote = (await adapterFor(intent.gateway)!.fetchIntent(intent.gatewayRef)).status;
if (local !== remote)
discrepancies.push({ id: intent.id, local, remote, gateway: intent.gateway });
}
return discrepancies;
}
return {
initializePayment,
getPayment,
listPayments,
checkPayment,
confirmPayment,
syncPayment,
capturePayment,
cancelPayment,
refundPayment,
listRefunds,
refundableAmount,
paymentMethods,
attachPaymentMethod,
detachPaymentMethod,
setDefaultPaymentMethod,
paymentTotals,
reconcilePayments,
paymentWebhookHandler,
capabilitiesOf: (id: string) => adapterFor(id)?.capabilities,
};
}
export type PaymentService = ReturnType<typeof definePayment>;
const SERVICE = Symbol.for("@wrnexus/payment:service:v1");
type PaymentGlobal = typeof globalThis & { [SERVICE]?: PaymentService };
export function configurePayment(options: PaymentOptions): PaymentService {
const service = definePayment(options);
(globalThis as PaymentGlobal)[SERVICE] = service;
return service;
}
export function getPaymentService(): PaymentService {
const service = (globalThis as PaymentGlobal)[SERVICE];
if (!service)
throw new Error("WRN-PAYMENT-SETUP: call configurePayment() during application startup");
return service;
}
+220
View File
@@ -0,0 +1,220 @@
import { getDb } from "@wrnexus/db";
import type { PaymentIntent, PaymentRefund, PaymentStore } from "./types.ts";
const parse = <T>(value: unknown, fallback: T): T => {
try {
return typeof value === "string" ? JSON.parse(value) : (value as T);
} catch {
return fallback;
}
};
export function databasePaymentStore(database = "default"): PaymentStore {
const db = () => getDb(database);
const intent = (row: any): PaymentIntent => ({
id: String(row.id),
subject: String(row.subject),
gateway: String(row.gateway),
gatewayRef: String(row.gateway_ref),
amount: Number(row.amount),
currency: String(row.currency),
idempotencyKey: String(row.idempotency_key),
createdAt: Number(row.created_at),
checkoutUrl: row.checkout_url ? String(row.checkout_url) : undefined,
client: parse(row.client_json, {}),
metadata: parse(row.metadata_json, {}),
});
const refund = (row: any): PaymentRefund => ({
id: String(row.id),
intentId: String(row.intent_id),
gatewayRef: String(row.gateway_ref ?? ""),
amount: Number(row.amount),
currency: String(row.currency),
reason: String(row.reason),
idempotencyKey: String(row.idempotency_key),
status: row.status,
createdAt: Number(row.created_at),
});
return {
async findIntentByKey(key) {
const row = await db().one("SELECT * FROM wrn_payment_intent WHERE idempotency_key = ?", [
key,
]);
return row ? intent(row) : null;
},
async putIntent(row) {
await db().exec(
"INSERT INTO wrn_payment_intent (id,subject,gateway,gateway_ref,amount,currency,idempotency_key,created_at,checkout_url,client_json,metadata_json) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
[
row.id,
row.subject,
row.gateway,
row.gatewayRef,
row.amount,
row.currency,
row.idempotencyKey,
row.createdAt,
row.checkoutUrl ?? null,
JSON.stringify(row.client),
JSON.stringify(row.metadata),
],
);
},
async getIntent(id) {
const row = await db().one("SELECT * FROM wrn_payment_intent WHERE id = ?", [id]);
return row ? intent(row) : null;
},
async listIntents(filter = {}) {
const clauses: string[] = [],
values: unknown[] = [];
if (filter.subject) {
clauses.push("subject = ?");
values.push(filter.subject);
}
if (filter.gateway) {
clauses.push("gateway = ?");
values.push(filter.gateway);
}
return (
await db().all(
`SELECT * FROM wrn_payment_intent${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at DESC`,
values,
)
).map(intent);
},
async appendEvent(row) {
try {
const result = await db().exec(
"INSERT INTO wrn_payment_event (id,intent_id,type,status,created_at,occurred_at,gateway_sequence,payload_json) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING",
[
row.id,
row.intentId,
row.type,
row.status,
row.at,
row.occurredAt ?? row.at,
row.sequence === undefined ? null : String(row.sequence),
JSON.stringify(row.payload ?? null),
],
);
return result.changes > 0;
} catch {
return false;
}
},
async events(id) {
return (
await db().all<any>(
"SELECT * FROM wrn_payment_event WHERE intent_id = ? ORDER BY occurred_at ASC, gateway_sequence ASC, id ASC",
[id],
)
).map((row) => ({
id: String(row.id),
intentId: String(row.intent_id),
type: String(row.type),
status: row.status,
at: Number(row.created_at),
occurredAt: Number(row.occurred_at ?? row.created_at),
sequence: row.gateway_sequence == null ? undefined : String(row.gateway_sequence),
payload: parse(row.payload_json, null),
}));
},
async reserveRefund(row, captured) {
try {
const result = await db().exec(
"INSERT INTO wrn_payment_refund (id,intent_id,gateway_ref,amount,currency,reason,idempotency_key,status,created_at) SELECT ?,?,?,?,?,?,?,?,? WHERE ? >= COALESCE((SELECT SUM(amount) FROM wrn_payment_refund WHERE intent_id = ? AND status <> 'failed'),0) + ?",
[
row.id,
row.intentId,
row.gatewayRef,
row.amount,
row.currency,
row.reason,
row.idempotencyKey,
row.status,
row.createdAt,
captured,
row.intentId,
row.amount,
],
);
return result.changes > 0;
} catch {
return false;
}
},
async updateRefund(id, status, gatewayRef) {
await db().exec("UPDATE wrn_payment_refund SET status = ?, gateway_ref = ? WHERE id = ?", [
status,
gatewayRef,
id,
]);
},
async refunds(id) {
return (
await db().all("SELECT * FROM wrn_payment_refund WHERE intent_id = ? ORDER BY created_at", [
id,
])
).map(refund);
},
async methods(subject) {
return (
await db().all<any>(
"SELECT * FROM wrn_payment_method WHERE subject = ? ORDER BY is_default DESC, created_at",
[subject],
)
).map((row) => ({
id: String(row.id),
subject: String(row.subject),
gateway: String(row.gateway),
gatewayRef: String(row.gateway_ref),
brand: row.brand ? String(row.brand) : undefined,
last4: row.last4 ? String(row.last4) : undefined,
expires: row.expires ? String(row.expires) : undefined,
default: Boolean(row.is_default),
}));
},
async getMethod(id) {
const row = await db().one<any>("SELECT * FROM wrn_payment_method WHERE id = ?", [id]);
return row
? {
id: String(row.id),
subject: String(row.subject),
gateway: String(row.gateway),
gatewayRef: String(row.gateway_ref),
brand: row.brand ? String(row.brand) : undefined,
last4: row.last4 ? String(row.last4) : undefined,
expires: row.expires ? String(row.expires) : undefined,
default: Boolean(row.is_default),
}
: null;
},
async putMethod(row) {
await db().exec(
"INSERT INTO wrn_payment_method (id,subject,gateway,gateway_ref,brand,last4,expires,is_default,created_at) VALUES (?,?,?,?,?,?,?,?,?)",
[
row.id,
row.subject,
row.gateway,
row.gatewayRef,
row.brand ?? null,
row.last4 ?? null,
row.expires ?? null,
row.default ? 1 : 0,
Date.now(),
],
);
},
async removeMethod(id) {
await db().exec("DELETE FROM wrn_payment_method WHERE id = ?", [id]);
},
async defaultMethod(subject, id) {
await db().tx(async (tx) => {
await tx.exec("UPDATE wrn_payment_method SET is_default = 0 WHERE subject = ?", [subject]);
await tx.exec("UPDATE wrn_payment_method SET is_default = 1 WHERE subject = ? AND id = ?", [
subject,
id,
]);
});
},
};
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./types.ts";
export * from "./store.ts";
export * from "./database.ts";
export * from "./core.ts";
export * from "./adapters.ts";
export * from "./contract.ts";
export { paymentPlugin } from "./plugin.ts";
+65
View File
@@ -0,0 +1,65 @@
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const extension = basename(dirname(fileURLToPath(import.meta.url))) === "dist" ? ".js" : ".ts";
const webhookEntry = join(dirname(fileURLToPath(import.meta.url)), `runtime-webhook${extension}`);
const migration = `-- +up
CREATE TABLE IF NOT EXISTS wrn_payment_intent (id TEXT PRIMARY KEY, subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, amount BIGINT NOT NULL, currency TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, created_at BIGINT NOT NULL, checkout_url TEXT, client_json TEXT NOT NULL, metadata_json TEXT NOT NULL);
CREATE INDEX IF NOT EXISTS wrn_payment_intent_subject ON wrn_payment_intent(subject, created_at);
CREATE TABLE IF NOT EXISTS wrn_payment_event (id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, type TEXT NOT NULL, status TEXT NOT NULL, created_at BIGINT NOT NULL, payload_json TEXT, FOREIGN KEY(intent_id) REFERENCES wrn_payment_intent(id));
CREATE INDEX IF NOT EXISTS wrn_payment_event_intent ON wrn_payment_event(intent_id, created_at, id);
CREATE TABLE IF NOT EXISTS wrn_payment_refund (id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, gateway_ref TEXT NOT NULL, amount BIGINT NOT NULL, currency TEXT NOT NULL, reason TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, status TEXT NOT NULL, created_at BIGINT NOT NULL, FOREIGN KEY(intent_id) REFERENCES wrn_payment_intent(id));
CREATE TABLE IF NOT EXISTS wrn_payment_method (id TEXT PRIMARY KEY, subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, brand TEXT, last4 TEXT, expires TEXT, is_default INTEGER NOT NULL DEFAULT 0, created_at BIGINT NOT NULL);
CREATE TABLE IF NOT EXISTS wrn_payment_customer (subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, created_at BIGINT NOT NULL, PRIMARY KEY(subject,gateway));
-- +down
DROP TABLE IF EXISTS wrn_payment_customer;
DROP TABLE IF EXISTS wrn_payment_method;
DROP TABLE IF EXISTS wrn_payment_refund;
DROP TABLE IF EXISTS wrn_payment_event;
DROP TABLE IF EXISTS wrn_payment_intent;`;
const orderingMigration = `-- +up
ALTER TABLE wrn_payment_event ADD COLUMN occurred_at BIGINT;
ALTER TABLE wrn_payment_event ADD COLUMN gateway_sequence TEXT;
UPDATE wrn_payment_event SET occurred_at = created_at WHERE occurred_at IS NULL;
CREATE INDEX IF NOT EXISTS wrn_payment_event_gateway_order ON wrn_payment_event(intent_id, occurred_at, gateway_sequence, id);
-- +down
DROP INDEX IF EXISTS wrn_payment_event_gateway_order;`;
export function paymentPlugin() {
const key = "@wrnexus/payment:enabled";
return definePlugin({
name: "@wrnexus/payment",
version: "0.8.1",
componentDirs: [join(root, "components")],
routeEntries(context) {
return context.metadata.get(key)
? [{ kind: "api" as const, path: "/api/payments/webhook/:gateway", entry: webhookEntry }]
: [];
},
configure(config, context) {
const value = config.payment as
{ default?: string; gateways?: Record<string, unknown>; sandbox?: boolean } | undefined;
context.metadata.set(key, Boolean(value));
if (!value) return;
if (!value.default) throw new Error("WRN-PAYMENT-CONFIG: payment.default is required");
if (!value.gateways?.[value.default] && value.default !== "sandbox")
throw new Error(
`WRN-PAYMENT-CONFIG: default gateway '${value.default}' has no configuration`,
);
if (process.env.NODE_ENV === "production" && value.sandbox)
throw new Error("WRN-PAYMENT-CONFIG: sandbox cannot be enabled in production");
},
migrations(context: PluginContext) {
return context.metadata.get(key)
? [
{ id: "wrnexus-payment-001", source: migration },
{ id: "wrnexus-payment-002-event-order", source: orderingMigration },
]
: [];
},
documentation: [join(root, "README.md")],
});
}
export default paymentPlugin;
+7
View File
@@ -0,0 +1,7 @@
import { getPaymentService } from "./core.ts";
export async function POST(ctx: { req: Request; params?: Record<string, string> }) {
const gateway = ctx.params?.gateway?.trim();
if (!gateway) return Response.json({ error: "payment gateway is required" }, { status: 400 });
return getPaymentService().paymentWebhookHandler(gateway)(ctx);
}
+101
View File
@@ -0,0 +1,101 @@
import type {
PaymentEvent,
PaymentIntent,
PaymentMethod,
PaymentRefund,
PaymentStore,
} from "./types.ts";
export function memoryPaymentStore(): PaymentStore {
const intents = new Map<string, PaymentIntent>();
const keys = new Map<string, string>();
const eventRows = new Map<string, PaymentEvent>();
const refundRows = new Map<string, PaymentRefund>();
const methodRows = new Map<string, PaymentMethod>();
let lock = Promise.resolve();
const exclusive = async <T>(run: () => T | Promise<T>): Promise<T> => {
const previous = lock;
let release!: () => void;
lock = new Promise<void>((resolve) => (release = resolve));
await previous;
try {
return await run();
} finally {
release();
}
};
return {
async findIntentByKey(key) {
const id = keys.get(key);
return id ? structuredClone(intents.get(id)!) : null;
},
async putIntent(intent) {
intents.set(intent.id, structuredClone(intent));
keys.set(intent.idempotencyKey, intent.id);
},
async getIntent(id) {
const row = intents.get(id);
return row ? structuredClone(row) : null;
},
async listIntents(filter = {}) {
return [...intents.values()]
.filter(
(row) =>
(!filter.subject || row.subject === filter.subject) &&
(!filter.gateway || row.gateway === filter.gateway),
)
.map((row) => structuredClone(row));
},
async appendEvent(event) {
if (eventRows.has(event.id)) return false;
eventRows.set(event.id, structuredClone(event));
return true;
},
async events(intentId) {
return [...eventRows.values()]
.filter((row) => row.intentId === intentId)
.sort((a, b) => a.at - b.at)
.map((row) => structuredClone(row));
},
async reserveRefund(refund, captured) {
return exclusive(() => {
if ([...refundRows.values()].some((row) => row.idempotencyKey === refund.idempotencyKey))
return false;
const used = [...refundRows.values()]
.filter((row) => row.intentId === refund.intentId && row.status !== "failed")
.reduce((sum, row) => sum + row.amount, 0);
if (used + refund.amount > captured) return false;
refundRows.set(refund.id, structuredClone(refund));
return true;
});
},
async updateRefund(id, status, gatewayRef) {
const row = refundRows.get(id);
if (row) refundRows.set(id, { ...row, status, gatewayRef });
},
async refunds(intentId) {
return [...refundRows.values()]
.filter((row) => row.intentId === intentId)
.map((row) => structuredClone(row));
},
async methods(subject) {
return [...methodRows.values()]
.filter((row) => row.subject === subject)
.map((row) => structuredClone(row));
},
async getMethod(id) {
const row = methodRows.get(id);
return row ? structuredClone(row) : null;
},
async putMethod(method) {
methodRows.set(method.id, structuredClone(method));
},
async removeMethod(id) {
methodRows.delete(id);
},
async defaultMethod(subject, id) {
for (const [key, row] of methodRows)
if (row.subject === subject) methodRows.set(key, { ...row, default: key === id });
},
};
}
+162
View File
@@ -0,0 +1,162 @@
export type PaymentStatus =
| "created"
| "pending"
| "authorized"
| "captured"
| "failed"
| "cancelled"
| "partially_refunded"
| "refunded";
export interface Money {
amount: number;
currency: string;
}
export interface GatewayCapabilities {
authorizeThenCapture: boolean;
partialRefund: boolean;
multipleRefunds: boolean;
storedMethods: boolean;
customerVault: boolean;
hostedFields: boolean;
hostedCheckout: boolean;
webhookSignature: boolean;
payouts: boolean;
disputes: boolean;
}
export interface GatewaySupport {
currencies: readonly string[] | "any";
countries: readonly string[] | "any";
}
export interface CreateIntentInput extends Money {
id: string;
subject: string;
idempotencyKey: string;
country?: string;
capture?: "automatic" | "manual";
metadata?: Record<string, string>;
returnUrl?: string;
}
export interface GatewayIntent {
ref: string;
status: PaymentStatus;
amount: number;
currency: string;
checkoutUrl?: string;
clientSecret?: string;
raw?: unknown;
}
export interface GatewayRefund {
ref: string;
status: "pending" | "succeeded" | "failed";
amount: number;
raw?: unknown;
}
export interface GatewayMethod {
ref: string;
brand?: string;
last4?: string;
expires?: string;
}
export interface CustomerInput {
subject: string;
email?: string;
name?: string;
}
export interface RefundInput extends Money {
intentRef: string;
idempotencyKey: string;
reason: string;
}
export interface WebhookEvent {
id: string;
intentRef: string;
type: string;
status: PaymentStatus;
/** Gateway-authored event time, never local arrival time. */
occurredAt: number;
/** Gateway-authored ordering token when the provider exposes one. */
sequence?: string | number;
payload: unknown;
}
export interface PaymentGateway<TConfig = unknown> {
readonly id: string;
readonly config?: TConfig;
readonly capabilities: GatewayCapabilities;
readonly supports: GatewaySupport;
createIntent(input: CreateIntentInput): Promise<GatewayIntent>;
fetchIntent(intentRef: string): Promise<GatewayIntent>;
cancelIntent(intentRef: string, reason: string): Promise<GatewayIntent>;
refund(input: RefundInput): Promise<GatewayRefund>;
verifyWebhook(req: Request, secret: string): Promise<WebhookEvent>;
capture?(intentRef: string, amount?: Money): Promise<GatewayIntent>;
listMethods?(customerRef: string): Promise<GatewayMethod[]>;
attachMethod?(customerRef: string, token: string): Promise<GatewayMethod>;
detachMethod?(methodRef: string): Promise<void>;
createCustomer?(input: CustomerInput): Promise<string>;
clientConfig(intent: GatewayIntent): Record<string, string>;
}
export interface PaymentIntent extends Money {
id: string;
subject: string;
gateway: string;
gatewayRef: string;
idempotencyKey: string;
createdAt: number;
checkoutUrl?: string;
client: Record<string, string>;
metadata: Record<string, string>;
}
export interface PaymentEvent {
id: string;
intentId: string;
type: string;
status: PaymentStatus;
at: number;
occurredAt?: number;
sequence?: string | number;
payload?: unknown;
}
export interface PaymentRefund {
id: string;
intentId: string;
gatewayRef: string;
amount: number;
currency: string;
reason: string;
idempotencyKey: string;
status: "pending" | "succeeded" | "failed";
createdAt: number;
}
export interface PaymentMethod {
id: string;
subject: string;
gateway: string;
gatewayRef: string;
brand?: string;
last4?: string;
expires?: string;
default: boolean;
}
export type PaymentResult<T> =
| { ok: true; value: T }
| { ok: false; fault: "refused" | "gateway" | "storage" | "configuration"; reason: string };
export interface PaymentStore {
findIntentByKey(key: string): Promise<PaymentIntent | null>;
putIntent(intent: PaymentIntent): Promise<void>;
getIntent(id: string): Promise<PaymentIntent | null>;
listIntents(filter?: { subject?: string; gateway?: string }): Promise<PaymentIntent[]>;
appendEvent(event: PaymentEvent): Promise<boolean>;
events(intentId: string): Promise<PaymentEvent[]>;
reserveRefund(refund: PaymentRefund, captured: number): Promise<boolean>;
updateRefund(id: string, status: PaymentRefund["status"], gatewayRef: string): Promise<void>;
refunds(intentId: string): Promise<PaymentRefund[]>;
methods(subject: string): Promise<PaymentMethod[]>;
getMethod(id: string): Promise<PaymentMethod | null>;
putMethod(method: PaymentMethod): Promise<void>;
removeMethod(id: string): Promise<void>;
defaultMethod(subject: string, id: string): Promise<void>;
}
@@ -0,0 +1,237 @@
import { afterEach, expect, test } from "bun:test";
import {
exerciseGatewayContract,
paypalGateway,
razorpayGateway,
sandboxGateway,
signSandboxWebhook,
stripeGateway,
type GatewayIntent,
} from "../src/index.ts";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
const json = (value: unknown, status = 200) =>
new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
const encoder = new TextEncoder();
async function signature(body: string, secret: string) {
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(body)))]
.map((x) => x.toString(16).padStart(2, "0"))
.join("");
}
const input = {
id: "contract-pay",
subject: "contract-user",
amount: 1000,
currency: "USD",
idempotencyKey: "contract-init",
};
test("shared executable contract drives sandbox", async () => {
const adapter = sandboxGateway();
const report = await exerciseGatewayContract(adapter, {
input,
async webhook(intent) {
const signed = await signSandboxWebhook(
{
id: "sb-event",
intentRef: intent.ref,
type: "payment.captured",
status: "captured",
occurredAt: 100,
},
"contract-webhook-secret",
);
return new Request("https://test", {
method: "POST",
headers: { "x-wrnexus-signature": signed.signature },
body: signed.body,
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
test("shared executable contract drives Stripe through its HTTP adapter", async () => {
globalThis.fetch = (async (value, init) => {
const url = String(value),
method = init?.method ?? "GET";
if (
url.includes("/payment_intents") &&
method === "POST" &&
!url.endsWith("/capture") &&
!url.endsWith("/cancel")
) {
const body = String(init?.body);
const id = body.includes("cancel") ? "pi_cancel" : "pi_contract";
return json({
id,
status: "requires_capture",
amount: 1000,
amount_received: 1000,
currency: "usd",
client_secret: "pi_client_safe",
});
}
if (url.endsWith("/capture"))
return json({
id: "pi_contract",
status: "succeeded",
amount_received: 1000,
currency: "usd",
});
if (url.endsWith("/cancel"))
return json({ id: "pi_cancel", status: "canceled", amount: 1000, currency: "usd" });
if (url.includes("/payment_intents/"))
return json({ id: "pi_contract", status: "requires_capture", amount: 1000, currency: "usd" });
if (url.endsWith("/refunds")) return json({ id: "re_1", status: "succeeded", amount: 500 });
if (url.endsWith("/customers")) return json({ id: "cus_1" });
if (url.includes("/payment_methods?")) return json({ data: [] });
if (url.includes("/payment_methods/") && url.endsWith("/attach"))
return json({
id: "pm_1",
card: { brand: "visa", last4: "4242", exp_month: 1, exp_year: 2030 },
});
if (url.includes("/payment_methods/") && url.endsWith("/detach")) return json({ id: "pm_1" });
return json({}, 404);
}) as typeof fetch;
const adapter = stripeGateway({
secretKey: "sk_test_secret",
publishableKey: "pk_test",
webhookSecret: "contract-webhook-secret",
});
const report = await exerciseGatewayContract(adapter, {
input,
async webhook(intent) {
const body = JSON.stringify({
id: "evt_1",
created: 100,
data: { object: { id: intent.ref, status: "succeeded" } },
type: "payment_intent.succeeded",
});
return new Request("https://test", {
method: "POST",
headers: {
"stripe-signature": `t=100,v1=${await signature(`100.${body}`, "contract-webhook-secret")}`,
},
body,
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
test("shared executable contract drives Razorpay through its HTTP adapter", async () => {
globalThis.fetch = (async (value, init) => {
const url = String(value),
method = init?.method ?? "GET";
if (url.endsWith("/orders") && method === "POST") {
const body = String(init?.body);
return json({
id: body.includes("cancel") ? "order_cancel" : "order_1",
status: "created",
amount: 1000,
currency: "USD",
});
}
if (url.includes("/orders/"))
return json({
id: url.endsWith("order_cancel") ? "order_cancel" : "order_1",
status: "created",
amount: 1000,
currency: "USD",
});
if (url.includes("/refund")) return json({ id: "rfnd_1", status: "processed", amount: 500 });
return json({}, 404);
}) as typeof fetch;
const adapter = razorpayGateway({
keyId: "rzp_test",
keySecret: "key_secret",
webhookSecret: "contract-webhook-secret",
});
const report = await exerciseGatewayContract(adapter, {
input,
async webhook(intent) {
const body = JSON.stringify({
id: "rz_evt",
event: "payment.captured",
payload: {
payment: {
entity: { id: "pay_1", order_id: intent.ref, status: "captured", created_at: 100 },
},
},
});
return new Request("https://test", {
method: "POST",
headers: { "x-razorpay-signature": await signature(body, "contract-webhook-secret") },
body,
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
test("shared executable contract drives PayPal through its HTTP adapter", async () => {
globalThis.fetch = (async (value, init) => {
const url = String(value),
method = init?.method ?? "GET";
if (url.endsWith("/oauth2/token")) return json({ access_token: "access" });
if (url.endsWith("/checkout/orders") && method === "POST") {
const body = String(init?.body);
return json({
id: body.includes("cancel") ? "PP-CANCEL" : "PP-ORDER",
status: "CREATED",
links: [{ rel: "approve", href: "https://paypal.test/approve" }],
});
}
if (url.includes("/checkout/orders/"))
return json({
id: url.endsWith("PP-CANCEL") ? "PP-CANCEL" : "PP-ORDER",
status: "CREATED",
purchase_units: [{ amount: { value: "10.00", currency_code: "USD" } }],
});
if (url.includes("/refund")) return json({ id: "PP-REFUND", status: "COMPLETED" });
if (url.endsWith("verify-webhook-signature")) return json({ verification_status: "SUCCESS" });
return json({}, 404);
}) as typeof fetch;
const adapter = paypalGateway({
clientId: "client",
clientSecret: "paypal_secret",
webhookId: "hook",
});
const report = await exerciseGatewayContract(adapter, {
input,
webhook(intent: GatewayIntent) {
return new Request("https://test", {
method: "POST",
headers: {
"paypal-auth-algo": "SHA256withRSA",
"paypal-cert-url": "https://paypal.test/cert",
"paypal-transmission-id": "tx",
"paypal-transmission-sig": "sig",
"paypal-transmission-time": "time",
},
body: JSON.stringify({
id: "WH-1",
create_time: "2026-08-23T00:00:00Z",
event_type: "PAYMENT.CAPTURE.COMPLETED",
resource: {
id: "CAPTURE",
status: "COMPLETED",
supplementary_data: { related_ids: { order_id: intent.ref } },
},
}),
});
},
});
expect(report).toEqual({ ok: true, issues: [] });
});
@@ -0,0 +1,111 @@
import { afterEach, expect, test } from "bun:test";
import { closeDatabases, setDb } from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { createPluginRunner } from "@wrnexus/plugin";
import { parse } from "@wrnexus/syntax";
import { readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { databasePaymentStore, paymentPlugin } from "../src/index.ts";
afterEach(() => closeDatabases());
test("payment plugin contributes its schema only when configured", async () => {
const metadata = new Map<string, unknown>();
const context = {
root: process.cwd(),
mode: "development" as const,
command: "dev" as const,
metadata,
warn() {},
};
const runner = createPluginRunner(paymentPlugin(), context);
await runner.configure({ payment: { default: "sandbox", sandbox: true } } as any);
const contributions = await runner.contributions();
expect(contributions.migrations.map((row) => row.id)).toEqual([
"wrnexus-payment-001",
"wrnexus-payment-002-event-order",
]);
expect(contributions.componentDirs).toHaveLength(1);
expect(contributions.routes.map((row) => row.path)).toEqual(["/api/payments/webhook/:gateway"]);
const off = createPluginRunner(paymentPlugin(), { ...context, metadata: new Map() });
await off.configure({} as any);
expect((await off.contributions()).migrations).toHaveLength(0);
expect((await off.contributions()).routes).toHaveLength(0);
});
test("database store persists append-only events and atomically caps refunds", async () => {
const db = connectFromConfig({ driver: "sqlite", url: ":memory:" });
setDb(db);
const metadata = new Map<string, unknown>();
const runner = createPluginRunner(paymentPlugin(), {
root: process.cwd(),
mode: "development",
command: "dev",
metadata,
warn() {},
});
await runner.configure({ payment: { default: "sandbox" } } as any);
for (const migration of (await runner.contributions()).migrations) {
const up = migration.source!.split("-- +down")[0].replace("-- +up", "");
for (const statement of up
.split(";")
.map((x) => x.trim())
.filter(Boolean))
await db.exec(statement);
}
const store = databasePaymentStore();
const intent = {
id: "pay-1",
subject: "u",
gateway: "sandbox",
gatewayRef: "sb-1",
amount: 100,
currency: "USD",
idempotencyKey: "once",
createdAt: 1,
client: {},
metadata: {},
};
await store.putIntent(intent);
expect((await store.findIntentByKey("once"))?.id).toBe("pay-1");
expect(
await store.appendEvent({
id: "e1",
intentId: "pay-1",
type: "captured",
status: "captured",
at: 1,
}),
).toBe(true);
expect(
await store.appendEvent({
id: "e1",
intentId: "pay-1",
type: "captured",
status: "captured",
at: 1,
}),
).toBe(false);
const base = {
intentId: "pay-1",
gatewayRef: "",
currency: "USD",
reason: "test",
status: "pending" as const,
createdAt: 1,
};
const results = await Promise.all([
store.reserveRefund({ id: "r1", amount: 60, idempotencyKey: "r1", ...base }, 100),
store.reserveRefund({ id: "r2", amount: 60, idempotencyKey: "r2", ...base }, 100),
]);
expect(results.filter(Boolean)).toHaveLength(1);
});
test("all payment components parse and never collect raw card data", () => {
for (const file of readdirSync(join(import.meta.dir, "../components")).filter((x) =>
x.endsWith(".wrn"),
)) {
const source = readFileSync(join(import.meta.dir, "../components", file), "utf8");
expect(() => parse(source)).not.toThrow();
expect(source).not.toMatch(/cardNumber|cvv|\bpan\b/i);
}
});
+240
View File
@@ -0,0 +1,240 @@
import { expect, test } from "bun:test";
import {
defineGateway,
definePayment,
memoryPaymentStore,
paypalGateway,
razorpayGateway,
sandboxGateway,
signSandboxWebhook,
stripeGateway,
derivePaymentStatus,
verifyGatewayContract,
} from "../src/index.ts";
test("gateway occurrence order and sticky terminal states prevent webhook regression", () => {
const events = [
{
id: "refund",
intentId: "p",
type: "refunded",
status: "refunded" as const,
at: 100,
occurredAt: 300,
},
{
id: "late-capture-retry",
intentId: "p",
type: "captured",
status: "captured" as const,
at: 400,
occurredAt: 200,
},
];
expect(derivePaymentStatus(events)).toBe("refunded");
expect(
derivePaymentStatus([...events, { ...events[1], id: "new-id", at: 500, occurredAt: 500 }]),
).toBe("refunded");
});
test("sandbox drives webhook-authoritative capture and bounded partial refunds", async () => {
const store = memoryPaymentStore(),
gateway = sandboxGateway({ secret: "test-secret" });
const payment = definePayment({
store,
adapters: [gateway],
default: "sandbox",
webhookSecrets: { sandbox: "test-secret" },
environment: "test",
});
const created = await payment.initializePayment({
subject: "user-1",
amount: 1000,
currency: "USD",
idempotencyKey: "checkout-1",
});
expect(created.ok).toBe(true);
if (!created.ok) return;
expect(await payment.checkPayment(created.value.id)).toBe("pending");
// A browser redirect cannot advance local state.
expect(await payment.checkPayment(created.value.id)).toBe("pending");
const signed = await signSandboxWebhook(
{
id: "evt-1",
intentRef: created.value.gatewayRef,
type: "payment.captured",
status: "captured",
},
"test-secret",
);
const handler = payment.paymentWebhookHandler("sandbox");
expect(
(
await handler({
req: new Request("https://app.test/webhook", {
method: "POST",
headers: { "x-wrnexus-signature": signed.signature },
body: signed.body,
}),
})
).status,
).toBe(200);
expect(await payment.checkPayment(created.value.id)).toBe("captured");
const first = await payment.refundPayment({
id: created.value.id,
amount: 400,
reason: "partial return",
idempotencyKey: "refund-1",
});
expect(first.ok).toBe(true);
expect((await payment.refundableAmount(created.value.id)).amount).toBe(600);
const excessive = await payment.refundPayment({
id: created.value.id,
amount: 601,
reason: "too much",
idempotencyKey: "refund-2",
});
expect(excessive).toMatchObject({ ok: false, fault: "refused" });
expect(
(
await payment.refundPayment({
id: created.value.id,
amount: 600,
reason: "remaining return",
idempotencyKey: "refund-remaining",
})
).ok,
).toBe(true);
const stale = await signSandboxWebhook(
{
id: "evt-delayed-capture",
intentRef: created.value.gatewayRef,
type: "payment.captured",
status: "captured",
occurredAt: 1,
},
"test-secret",
);
await handler({
req: new Request("https://app.test/webhook", {
method: "POST",
headers: { "x-wrnexus-signature": stale.signature },
body: stale.body,
}),
});
expect(await payment.checkPayment(created.value.id)).toBe("refunded");
});
test("initialize, webhook, and refund replay change records once", async () => {
const store = memoryPaymentStore(),
gateway = sandboxGateway();
const payment = definePayment({
store,
adapters: [gateway],
default: "sandbox",
webhookSecrets: { sandbox: "sandbox-secret" },
});
const input = { subject: "user-2", amount: 500, currency: "EUR", idempotencyKey: "same" };
const one = await payment.initializePayment(input),
two = await payment.initializePayment(input);
expect(one.ok && two.ok && one.value.id).toBe(two.ok ? two.value.id : "");
if (!one.ok) return;
const signed = await signSandboxWebhook({
id: "evt-replay",
intentRef: one.value.gatewayRef,
type: "payment.captured",
status: "captured",
});
const request = () =>
new Request("https://app.test/webhook", {
method: "POST",
headers: { "x-wrnexus-signature": signed.signature },
body: signed.body,
});
const handler = payment.paymentWebhookHandler("sandbox");
await handler({ req: request() });
const replay = await handler({ req: request() });
expect(await replay.json()).toEqual({ received: true, duplicate: true });
const refund = await payment.refundPayment({
id: one.value.id,
amount: 100,
reason: "return",
idempotencyKey: "same-refund",
});
const duplicate = await payment.refundPayment({
id: one.value.id,
amount: 100,
reason: "return",
idempotencyKey: "same-refund",
});
expect(refund.ok && duplicate.ok && refund.value.id).toBe(duplicate.ok ? duplicate.value.id : "");
expect(await payment.listRefunds(one.value.id)).toHaveLength(1);
});
test("selection and capability differences are refused before money moves", async () => {
const sandbox = sandboxGateway();
const noPartial = defineGateway({
...sandbox,
id: "full-only",
capabilities: { ...sandbox.capabilities, partialRefund: false },
});
const payment = definePayment({
store: memoryPaymentStore(),
adapters: [sandbox, noPartial],
default: "sandbox",
route: (input) => (input.currency === "INR" ? "full-only" : "sandbox"),
});
const manual = await payment.initializePayment({
subject: "u",
amount: 100,
currency: "INR",
country: "IN",
capture: "manual",
idempotencyKey: "route",
});
expect(manual.ok).toBe(true);
expect(verifyGatewayContract(sandbox)).toEqual({ ok: true, issues: [] });
const broken = defineGateway({
...sandbox,
id: "broken",
capabilities: { ...sandbox.capabilities, authorizeThenCapture: false },
});
expect(verifyGatewayContract(broken).ok).toBe(false);
});
test("invalid webhook signatures never update payment status", async () => {
const gateway = sandboxGateway(),
payment = definePayment({
store: memoryPaymentStore(),
adapters: [gateway],
default: "sandbox",
webhookSecrets: { sandbox: "right" },
});
const created = await payment.initializePayment({
subject: "u",
amount: 100,
currency: "USD",
idempotencyKey: "bad-hook",
});
if (!created.ok) return;
const response = await payment.paymentWebhookHandler("sandbox")({
req: new Request("https://app.test", {
method: "POST",
headers: { "x-wrnexus-signature": "wrong" },
body: JSON.stringify({ id: "bad", intentRef: created.value.gatewayRef, status: "captured" }),
}),
});
expect(response.status).toBe(400);
expect(await payment.checkPayment(created.value.id)).toBe("pending");
});
test("every Tier 1 adapter passes the shared capability contract", () => {
const adapters = [
sandboxGateway(),
stripeGateway({ secretKey: "sk_test", publishableKey: "pk_test", webhookSecret: "whsec" }),
razorpayGateway({ keyId: "rzp_test", keySecret: "secret", webhookSecret: "hook" }),
paypalGateway({ clientId: "client", clientSecret: "secret", webhookId: "hook" }),
];
for (const adapter of adapters)
expect(verifyGatewayContract(adapter)).toEqual({ ok: true, issues: [] });
});
+9 -9
View File
@@ -37,15 +37,15 @@ function createQueue(options?: QueueOptions): Queue;
#### `QueueOptions`
| Option | Type | Default | Description |
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
| `capacity` | `number` | unlimited | Maximum queued plus active jobs before adds reject. |
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
| Option | Type | Default | Description |
| ------------- | ------------------------------------ | ---------- | --------------------------------------------------------------------------------------- |
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
| `capacity` | `number` | unlimited | Optional maximum queued jobs; omitted queues never scan merely to enforce a hidden cap. |
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
### `Queue`
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/queue",
"version": "0.8.13",
"version": "0.8.14",
"private": true,
"type": "module",
"main": "src/index.ts",
+1 -1
View File
@@ -46,7 +46,7 @@ function lazyStore(resolve: () => Promise<QueueStore>): QueueStore {
return (await ready()).list(name);
},
async findByIdempotencyKey(name, key) {
return (await ready()).findByIdempotencyKey?.(name, key) ?? null;
return (await ready()).findByIdempotencyKey(name, key);
},
async size() {
return (await ready()).size?.() ?? (await (await ready()).list()).length;
+9 -6
View File
@@ -14,7 +14,8 @@ export interface QueueStore {
remove(id: string): Promise<void>;
due(now: number, limit: number): Promise<Job[]>;
list(name?: string): Promise<Job[]>;
findByIdempotencyKey?(name: string, key: string): Promise<Job | null>;
/** Indexed lookup; implementations must not scan the queue. */
findByIdempotencyKey(name: string, key: string): Promise<Job | null>;
size?(): Promise<number>;
claim?(id: string, worker: string, leaseUntil: number, now?: number): Promise<boolean>;
release?(id: string, worker: string): Promise<void>;
@@ -175,7 +176,8 @@ export function createDurableQueue(options: DurableQueueOptions = {}): DurableQu
const workerId = options.workerId?.trim() || `worker-${crypto.randomUUID()}`;
const defaultMaxAttempts = positiveInteger(options.maxAttempts ?? 3, "queue maxAttempts");
const concurrency = positiveInteger(options.concurrency ?? 10, "queue concurrency");
const capacity = positiveInteger(options.capacity ?? 10_000, "queue capacity");
const capacity = options.capacity ?? Number.POSITIVE_INFINITY;
if (capacity !== Number.POSITIVE_INFINITY) positiveInteger(capacity, "queue capacity");
const leaseMs = positiveInteger(options.leaseMs ?? 30_000, "queue leaseMs");
const pollMs = positiveInteger(options.pollMs ?? 250, "queue pollMs");
let sequence = 0;
@@ -280,12 +282,13 @@ export function createDurableQueue(options: DurableQueueOptions = {}): DurableQu
}
if (add.idempotencyKey) {
const existing = store.findByIdempotencyKey
? await store.findByIdempotencyKey(name, add.idempotencyKey)
: (await store.list(name)).find((job) => job.idempotencyKey === add.idempotencyKey);
const existing = await store.findByIdempotencyKey(name, add.idempotencyKey);
if (existing) return existing as Job<T>;
}
if ((store.size ? await store.size() : (await store.list()).length) >= capacity) {
if (
capacity !== Number.POSITIVE_INFINITY &&
(store.size ? await store.size() : (await store.list()).length) >= capacity
) {
throw new Error(`WRN-QUEUE-CAPACITY: queue capacity of ${capacity} reached`);
}
+19 -8
View File
@@ -23,9 +23,19 @@ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queu
const jobs = `${prefix}:jobs`;
const due = `${prefix}:due`;
const key = (id: string) => `${prefix}:job:${id}`;
const idempotencyKey = (name: string, value: string) =>
`${prefix}:idempotency:${encodeURIComponent(name)}:${encodeURIComponent(value)}`;
return {
async put(job) {
const previousValue = await client.get(key(job.id));
const previous = previousValue ? (JSON.parse(previousValue) as Job) : null;
if (previous?.idempotencyKey && previous.idempotencyKey !== job.idempotencyKey) {
await client.del(idempotencyKey(previous.name, previous.idempotencyKey));
}
await client.set(key(job.id), JSON.stringify(job));
if (job.idempotencyKey) {
await client.set(idempotencyKey(job.name, job.idempotencyKey), job.id);
}
await client.sadd(jobs, job.id);
await client.zadd(due, job.runAt, job.id);
await client.del(`${prefix}:lease:${job.id}`);
@@ -35,7 +45,11 @@ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queu
return value ? (JSON.parse(value) as Job) : null;
},
async remove(id) {
await client.del(key(id), `${prefix}:lease:${id}`);
const value = await client.get(key(id));
const job = value ? (JSON.parse(value) as Job) : null;
const keys = [key(id), `${prefix}:lease:${id}`];
if (job?.idempotencyKey) keys.push(idempotencyKey(job.name, job.idempotencyKey));
await client.del(...keys);
await client.srem(jobs, id);
await client.zrem(due, id);
},
@@ -55,13 +69,10 @@ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queu
.filter((job) => !name || job.name === name);
},
async findByIdempotencyKey(name, idempotencyKey) {
const ids = await client.smembers(jobs);
const values = await Promise.all(ids.map((id) => client.get(key(id))));
const found = values
.filter((value): value is string => value !== null)
.map((value) => JSON.parse(value) as Job)
.find((job) => job.name === name && job.idempotencyKey === idempotencyKey);
return found ?? null;
const id = await client.get(
`${prefix}:idempotency:${encodeURIComponent(name)}:${encodeURIComponent(idempotencyKey)}`,
);
return id ? this.get(id) : null;
},
async size() {
return (await client.smembers(jobs)).length;
+15
View File
@@ -260,6 +260,21 @@ test("durable queue supports capacity, cancellation, and shutdown", async () =>
await expect(queue.add("work", {})).rejects.toThrow("WRN-QUEUE-CLOSED");
});
test("durable queue has no implicit capacity scan or 10k ceiling", async () => {
const base = memoryQueueStore();
let sizeCalls = 0;
const store = {
...base,
async size() {
sizeCalls++;
return 10_001;
},
};
const queue = createDurableQueue({ store });
await expect(queue.add("work", {})).resolves.toMatchObject({ name: "work" });
expect(sizeCalls).toBe(0);
});
test("force shutdown aborts an active durable job", async () => {
const store = memoryQueueStore();
const queue = createDurableQueue({ store });
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/store",
"version": "0.8.9",
"version": "0.8.12",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -0,0 +1,62 @@
import { expect, test } from "bun:test";
import { StoreContainer, defineStore } from "../src/index.ts";
test("memory persistence includes only approved fields and migrates old state", async () => {
const original = defineStore({
name: "preferences-migration",
kind: "global" as const,
createSharedState: () => ({ theme: "dark", secret: "initial" }),
persist: { storage: "memory" as const, version: 1, include: ["theme"] },
actions: {
setTheme: {
runtime: "client" as const,
handler: ({ state }: any, theme: string) => void (state.theme = theme),
},
},
});
const first = await new StoreContainer({ runtime: "client" }).use(original);
await first.actions.setTheme("light");
const migrated = defineStore({
...original,
persist: {
storage: "memory" as const,
version: 2,
include: ["theme"],
migrate: (state: any) => ({ ...state, theme: `${state.theme}-v2` }),
},
});
const second = await new StoreContainer({ runtime: "client" }).use(migrated);
expect(second.state.theme).toBe("light-v2");
expect(second.state.secret).toBe("initial");
});
test("invalid persisted state fails closed to the store defaults", async () => {
const name = `preferences-validation-${crypto.randomUUID()}`;
const firstDefinition = defineStore({
name,
kind: "global" as const,
createSharedState: () => ({ theme: "dark" }),
persist: { storage: "memory" as const, version: 1, include: ["theme"] },
actions: {
setTheme: {
runtime: "client" as const,
handler: ({ state }: any, theme: string) => void (state.theme = theme),
},
},
});
const first = await new StoreContainer({ runtime: "client" }).use(firstDefinition);
await first.actions.setTheme("corrupt");
const validated = defineStore({
...firstDefinition,
persist: {
storage: "memory" as const,
version: 1,
include: ["theme"],
validate: (value: any) => (value?.theme === "light" ? value : null),
},
});
const second = await new StoreContainer({ runtime: "client" }).use(validated);
expect(second.state.theme).toBe("dark");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.8.19",
"version": "0.8.20",
"type": "module",
"main": "src/index.ts",
"exports": {
+8
View File
@@ -313,6 +313,14 @@ export interface AppConfig {
queue?: string | boolean;
sandbox?: { enabled?: boolean; allowlist?: string[] };
};
/** Payment gateway configuration consumed by @wrnexus/payment. */
payment?: {
default: string;
currency?: string;
sandbox?: boolean;
databaseName?: string;
gateways?: Record<string, Record<string, unknown>>;
};
/**
* File-upload storage. Declare named stores (local dir or S3-compatible),
* upload with `handleUpload`/`upload` from `@wrnexus/uploader`, and serve
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/syntax",
"version": "0.8.13",
"version": "0.8.18",
"type": "module",
"main": "src/index.ts",
"exports": {
+34
View File
@@ -767,6 +767,28 @@ function expandStructuredStateDeclarations(lines, unit) {
});
}
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(
value,
);
if (!match) return null;
try {
const parsed = JSON.parse(match[2].trim());
const compact = JSON.stringify(parsed);
const indentation = unit.repeat(depth);
if (indentation.length + match[1].length + compact.length <= printWidth) return null;
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
return [
`${indentation}${match[1]}${jsonLines[0]}`,
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`),
];
} catch {
return null;
}
}
function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
@@ -844,6 +866,18 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
continue;
}
const structuredProp = formatStructuredPropDeclaration(
value,
unit,
codeDepth + htmlDepth + controlDepth,
printWidth,
);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value);
+2
View File
@@ -2,6 +2,8 @@ export { Lexer, LexError, skipLiteralOrComment } from "./tokenizer.ts";
export { formatWrn } from "./formatter.ts";
export type { FormatWrnOptions } from "./formatter.ts";
export { parse, parseHtmlView, ParseError, VOID_ELEMENTS } from "./parser.ts";
export { extractInterpolations } from "./interpolation.ts";
export type { InterpolationSegment } from "./interpolation.ts";
export type {
ActionBlock,
ApiBlock,
+39
View File
@@ -0,0 +1,39 @@
export interface InterpolationSegment {
expression: string;
start: number;
end: number;
}
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
export function extractInterpolations(value: string): InterpolationSegment[] {
const segments: InterpolationSegment[] = [];
let index = 0;
while (index < value.length) {
if (value[index] !== "{") {
index++;
continue;
}
const start = index++;
let depth = 1;
let quote: string | null = null;
while (index < value.length && depth > 0) {
const char = value[index]!;
if (quote) {
if (char === "\\" && index + 1 < value.length) index += 2;
else {
if (char === quote) quote = null;
index++;
}
continue;
}
if (char === '"' || char === "'" || char === "`") quote = char;
else if (char === "{") depth++;
else if (char === "}") depth--;
index++;
}
if (depth === 0) {
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
}
}
return segments;
}
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test";
import { hasRequestSection, parseApiEntries, parseApiSections } from "../src/api-sections.ts";
test("api section scanning ignores keywords in literals, comments, and nested objects", () => {
const source = `
// request { body { forged: string } }
response {
return { label: "request { not a section }", nested: { body: true } }
}
`;
expect(hasRequestSection(source)).toBe(false);
expect(parseApiSections(source)?.response).toContain("nested");
});
test("api entries normalize methods and reject malformed request fields", () => {
expect(
parseApiEntries(`lookup get /users/:id {
response { return data }
}`)[0],
).toMatchObject({
name: "lookup",
method: "GET",
path: "/users/:id",
});
expect(() =>
parseApiSections("request { body { missingType } } response { return data }"),
).toThrow('Expected "name: type"');
});
test("api entries reject truncated blocks and bare response bodies", () => {
expect(() => parseApiEntries("lookup GET /users { response { return data }")).toThrow();
expect(() => parseApiEntries("lookup GET /users { return data }")).toThrow(
'declare a "response { }" section',
);
});
+15
View File
@@ -83,3 +83,18 @@ test("preserves balanced compact sibling markup without indentation drift", () =
expect(formatted).toContain('<div class="strip"><span>Page</span><b>→</b><span>API</span></div>');
expect(formatWrn(formatted, { tabSize: 2, printWidth: 80 })).toBe(formatted);
});
test("formats large typed JSON prop defaults and remains idempotent", () => {
const source = `component Catalog {
props {
categories: unknown[] = [{"label":"Platform & Core","value":"platform","apps":[{"label":"WRNexus Home / Business Command Center","href":"/products/home","number":"01"},{"label":"SSO & Account","href":"/products/sso","number":"02"}]},{"label":"Sales & Customer","value":"sales","apps":[{"label":"CRM","href":"/products/crm","number":"11"}]}]
}
view { <div></div> }
}`;
const formatted = formatWrn(source, options);
expect(formatted).toContain(`categories: unknown[] = [\n {\n "label": "Platform & Core"`);
expect(formatted).toContain(`"apps": [\n {`);
expect(formatWrn(formatted, options)).toBe(formatted);
expect(() => parse(formatted)).not.toThrow();
});
+12 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { diagnose, formatDiagnostic, parse } from "../src/index.ts";
import { diagnose, extractInterpolations, formatDiagnostic, parse } from "../src/index.ts";
test("parses explicit rendering modes and the never hydration alias", () => {
const ast = parse(`page Marketing {
@@ -337,3 +337,14 @@ test("a block comment inside props is still refused with its own message", () =>
`;
expect(() => parse(source)).toThrow("Block comments are not allowed inside props");
});
test("extracts nested object and array interpolation as one expression", () => {
const value = `prefix {[{ label: 'One', meta: { count: 1 } }, { label: "Two" }]} suffix`;
expect(extractInterpolations(value)).toEqual([
{
expression: `[{ label: 'One', meta: { count: 1 } }, { label: "Two" }]`,
start: 7,
end: value.length - 7,
},
]);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/test",
"version": "0.8.11",
"version": "0.8.12",
"private": true,
"type": "module",
"main": "src/index.ts",
+2
View File
@@ -205,3 +205,5 @@ export type {
} from "./advanced.ts";
export { withDatabaseRollback, createFactory, captureBrowserArtifacts } from "./platform.ts";
export type { TransactionalDatabase, BrowserArtifactPage } from "./platform.ts";
export { detectMutations } from "./mutation.ts";
export type { MutationCase, MutationReport, DetectMutationsOptions } from "./mutation.ts";
+51
View File
@@ -0,0 +1,51 @@
export interface MutationCase<T> {
name: string;
value: T;
}
export interface MutationReport<O> {
baseline: O;
killed: string[];
survived: string[];
}
export interface DetectMutationsOptions<T, O> {
baseline: T;
mutations: Array<MutationCase<T>>;
exercise(value: T): O | Promise<O>;
equivalent?(left: O, right: O): boolean;
}
/**
* Run the same behavioural probe against a baseline and explicit mutations.
* A mutation survives when the observable result is unchanged. The helper
* throws with every survivor by default, making it suitable for an ordinary
* Bun test and CI without a second runner or source-rewriting process.
*/
export async function detectMutations<T, O>(
options: DetectMutationsOptions<T, O>,
): Promise<MutationReport<O>> {
if (!options.mutations.length) {
throw new Error("WRN-MUTATION-EMPTY: declare at least one meaningful mutation");
}
const equivalent = options.equivalent ?? ((left, right) => Object.is(left, right));
const baseline = await options.exercise(options.baseline);
const killed: string[] = [];
const survived: string[] = [];
for (const mutation of options.mutations) {
if (!mutation.name.trim()) throw new Error("WRN-MUTATION-NAME: every mutation needs a name");
let same = false;
try {
same = equivalent(baseline, await options.exercise(mutation.value));
} catch {
// A behavioural failure is a killed mutation.
}
(same ? survived : killed).push(mutation.name);
}
if (survived.length) {
throw Object.assign(new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`), {
report: { baseline, killed, survived },
});
}
return { baseline, killed, survived };
}
+35
View File
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test";
import { detectMutations } from "../src/mutation.ts";
test("detectMutations reports mutations whose behaviour changes", async () => {
const report = await detectMutations({
baseline: (amount: number) => amount,
mutations: [
{ name: "invert sign", value: (amount: number) => -amount },
{ name: "drop amount", value: () => 0 },
],
exercise: (adjust) => adjust(7),
});
expect(report.killed).toEqual(["invert sign", "drop amount"]);
expect(report.survived).toEqual([]);
});
test("detectMutations fails with the names of surviving mutations", async () => {
await expect(
detectMutations({
baseline: (amount: number) => amount,
mutations: [{ name: "wrong implementation", value: (amount: number) => amount }],
exercise: (adjust) => adjust(7),
}),
).rejects.toThrow(/WRN-MUTATION-SURVIVED: wrong implementation/);
});
test("detectMutations accepts structured observations through an equivalence function", async () => {
const report = await detectMutations({
baseline: { status: 200 },
mutations: [{ name: "deny request", value: { status: 403 } }],
exercise: async (response) => ({ ...response, volatile: crypto.randomUUID() }),
equivalent: (left, right) => left.status === right.status,
});
expect(report.killed).toEqual(["deny request"]);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/typecheck",
"version": "0.8.13",
"version": "0.8.16",
"type": "module",
"main": "src/index.ts",
"exports": {
+46 -6
View File
@@ -3,6 +3,7 @@ import { dirname, join, normalize, resolve } from "node:path";
import ts from "typescript";
import {
containsReadonlyPropMutation,
extractInterpolations,
inferredRuntimeType,
parse,
runtimeTypeOf,
@@ -139,7 +140,7 @@ function functionDeclaration(fn: RuntimeFunctionDecl): string {
const params = fn.parameters
.map(
(param) =>
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": Event & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
)
.join(", ");
// An omitted WRN parameter type follows JavaScript semantics. Emitting it as
@@ -214,6 +215,31 @@ function safeBindingName(name: string): boolean {
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
}
function safeViewExpression(expression: string, reservedProps: ReadonlySet<string>): string {
if (!reservedProps.size) return expression;
const scanner = ts.createScanner(
ts.ScriptTarget.Latest,
false,
ts.LanguageVariant.Standard,
expression,
);
let result = "";
let copied = 0;
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
const start = scanner.getTokenPos();
const end = scanner.getTextPos();
const text = scanner.getTokenText();
if (!reservedProps.has(text)) continue;
const previous = expression.slice(0, start).trimEnd().at(-1);
const following = expression.slice(end).trimStart().at(0);
// Property access and object-literal keys are already valid TypeScript.
if (previous === "." || following === ":") continue;
result += expression.slice(copied, start) + `props.${text}`;
copied = end;
}
return result + expression.slice(copied);
}
export function virtualTypeScriptModule(
source: string,
filePath = "component.wrn",
@@ -307,6 +333,9 @@ export function virtualTypeScriptModule(
if (ast.dataApis.length) append(`declare const api: { ${apiType} };`);
append(`declare const props: Readonly<${ast.name}Props>;`);
append("declare const refs: Record<string, Element | null>;");
append(
"declare const toast: ((message: string, options?: Record<string, unknown>) => unknown) & { success(message: string, options?: Record<string, unknown>): unknown; error(message: string, options?: Record<string, unknown>): unknown; danger(message: string, options?: Record<string, unknown>): unknown; warning(message: string, options?: Record<string, unknown>): unknown; info(message: string, options?: Record<string, unknown>): unknown; dismiss(id: string | number): void; clear(): void };",
);
append(
"declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;",
);
@@ -348,21 +377,29 @@ export function virtualTypeScriptModule(
// misspelled state/function name cannot silently become `undefined` at
// runtime. Each/handler locals mirror the browser evaluator's bindings.
let bindingIndex = 0;
const reservedProps = new Set(
ast.props.map((prop) => prop.name).filter((name) => !safeBindingName(name)),
);
const appendViewBindings = (nodes: ViewNode[], locals: Set<string>): void => {
for (const node of nodes) {
if (node.type === "element") {
for (const attr of node.attrs) {
const expressions = attr.event
? [attr.value]
: [...attr.value.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]!.trim());
: extractInterpolations(attr.value).map((segment) => segment.expression);
for (const expression of expressions) {
if (!expression.trim()) continue;
const declarations = [
...[...locals].map((name) => `declare const ${name}: any;`),
...(attr.event ? ["declare const payload: any;", "declare const event: Event;"] : []),
...(attr.event
? [
"declare const payload: any;",
"declare const event: Event & { target: HTMLElement };",
]
: []),
].join(" ");
append(
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${expression}); }`,
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${safeViewExpression(expression, reservedProps)}); }`,
attr.value,
);
}
@@ -372,13 +409,16 @@ export function virtualTypeScriptModule(
for (const branch of node.branches) {
if (branch.cond)
append(
`namespace __wrn_view_${bindingIndex++} { void (${branch.cond}); }`,
`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(branch.cond, reservedProps)}); }`,
branch.cond,
);
appendViewBindings(branch.body, locals);
}
} else if (node.type === "each") {
append(`namespace __wrn_view_${bindingIndex++} { void (${node.list}); }`, node.list);
append(
`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(node.list, reservedProps)}); }`,
node.list,
);
const nested = new Set(locals);
nested.add(node.item);
if (node.index) nested.add(node.index);
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { checkWrnSource } from "../src/index.ts";
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { checkWrnSource } from "../src/index.ts";
@@ -82,6 +82,23 @@ page Home {
expect(diagnostics.some((item) => item.code === "WRN-COMPONENT-PROP-TYPE")).toBe(false);
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1389")).toBe(false);
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1005")).toBe(false);
expect(
checkWrnSource(readFileSync(componentPath, "utf8"), { appRoot: root, filePath: componentPath }),
).toEqual([]);
}, 15_000);
test("implicit DOM events satisfy untyped event handler parameters", () => {
const root = fixture();
const filePath = join(root, "app", "components", "Submit.wrn");
const diagnostics = checkWrnSource(
`component Submit {
outputs { submit(payload: { event: Event }) }
functions { client function submitForm(event) { output.submit({ event }) } }
view { <form @submit="submitForm(event)"></form> }
}`,
{ appRoot: root, filePath },
);
expect(diagnostics).toEqual([]);
}, 15_000);
test("does not treat UI component imports as JavaScript exports", () => {
+29
View File
@@ -112,3 +112,32 @@ test("accepts handler payload, loop locals, and the public useFetch helper", ()
);
expect(diagnostics.filter((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toEqual([]);
});
test("accepts multiline array props containing nested object literals", () => {
const root = app();
const diagnostics = checkWrnSource(
`page Contact {
view {
<div data-component="StatsBar" items="{[
{ label: 'Product catalog', value: '81', suffix: ' apps' },
{ label: 'Starting point', value: 'One workflow' }
]}"></div>
}
}`,
{ appRoot: root, filePath: join(root, "app", "pages", "contact.wrn") },
);
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
});
test("provides the toast API to view event expressions", () => {
const root = app();
const diagnostics = checkWrnSource(
`page Contact {
view {
<form @wrn:success="toast.success('Sent', { title: 'Request sent' })"></form>
}
}`,
{ appRoot: root, filePath: join(root, "app", "pages", "contact.wrn") },
);
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
});
+15 -15
View File
@@ -45,7 +45,7 @@ Secure multi-cell PIN and verification-code input with regex and paste support.
Theme-aware, responsive strong password component.
- Mount: `data-component="StrongPassword"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `showLabel: string = "Show password"`, `hideLabel: string = "Hide password"`, `class: string = ""`
- Slots: None
- Outputs: `input({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `change({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `strength({ value: string; score: number; maximumScore: number; percent: number; level: string })`
@@ -146,7 +146,7 @@ Theme-aware, responsive button group component.
Group related content in a responsive themed surface with title, description, content, and supporting slots.
- Mount: `data-component="Card"`
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Props: `title: string = ""`, `icon: string = ""`, `titleKey: string = ""`, `subtitle: string = ""`, `subtitleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionLabelKey: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `radius: string = "default"`, `shadow: string = "default"`, `equalHeight: boolean = false`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
- Slots: `default`
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `navigate({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
@@ -337,7 +337,7 @@ Reusable portal dashboard component.
Reusable preference switcher component.
- Mount: `data-component="PreferenceSwitcher"`
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `iconOnly: boolean = true`, `themeIcon: string = "icon-[lucide--sun-moon]"`, `colorIcon: string = "icon-[lucide--palette]"`, `languageIcon: string = "icon-[lucide--languages]"`, `panelPlacement: string = "bottom-end"`, `mobilePanel: string = "dropdown"`, `showCurrentLanguage: boolean = true`, `class: string = ""`
- Slots: None
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
@@ -375,7 +375,7 @@ Arrange operational metrics, KPIs, public statistics, or service indicators in a
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
- Mount: `data-component="StatsBar"`
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `radius: string = "default"`, `shadow: string = "default"`, `gutter: string = "md"`, `outerSpacing: string = "none"`, `mobileColumns: number = 1`, `class: string = ""`
- Slots: None
- Outputs: None
@@ -606,7 +606,7 @@ Arrange feature cards, services, solutions, or benefits in a responsive equal-he
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
- Mount: `data-component="Footer"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `mobileColumns: number = 2`, `mobileCopyrightAlign: string = "center"`, `copyright: string = ""`, `copyrightKey: string = ""`, `translationPrefix: string = ""`, `class: string = ""`
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
@@ -698,7 +698,7 @@ Apply consistent readable responsive typography, widths, columns, spacing, and e
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
- Mount: `data-component="AnnouncementBar"`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `flush: boolean = false`, `mobileCompact: boolean = false`, `mobileMessageLines: number = 2`, `hideBadgeOnMobile: boolean = false`, `hideDescriptionOnMobile: boolean = false`, `mobileActionSize: string = "default"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
- Slots: None
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
@@ -707,7 +707,7 @@ Publish a responsive notice with badge, icon, supporting copy, action, dismiss b
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
- Mount: `data-component="CTASection"`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `spacing: string = "none"`, `radius: string = "default"`, `titleMaxWidth: string = ""`, `contentPadding: string = "default"`, `mobilePadding: string = "default"`, `mobileActions: string = "stack"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
- Slots: `default`, `actions`, `visual`, `footer`
- Outputs: None
@@ -725,7 +725,7 @@ Present one linked feature or service with media, icon, badge, description, and
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
- Mount: `data-component="FeatureIconCard"`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `radius: string = "default"`, `shadow: string = "default"`, `clickableSurface: boolean = false`, `descriptionTone: string = "default"`, `class: string = ""`
- Slots: `default`, `footer`
- Outputs: None
@@ -770,7 +770,7 @@ Introduce an internal or public page with breadcrumbs, icon, title, description,
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
- Mount: `data-component="SplitHero"`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `titleSize: string = "default"`, `titleMaxWidth: string = ""`, `descriptionWeight: string = "default"`, `mobileTitleSize: string = "default"`, `mobileStack: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
- Slots: `default`, `actions`, `trust`, `visual`
- Outputs: None
@@ -799,7 +799,7 @@ Provide a responsive floating control that returns long pages to the top and can
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
- Mount: `data-component="Breadcrumb"`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `activeIcon: string = "icon-[lucide--file-text]"`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
- Slots: None
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
@@ -808,9 +808,9 @@ Show responsive hierarchical navigation with home support, separators, current-p
Theme-aware, responsive mega menu component.
- Mount: `data-component="MegaMenu"`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `rail: unknown[] = []`, `featured: Record<string, unknown> = {}`, `variant: string = "default"`, `density: string = "default"`, `panelWidth: string = "xl"`, `panelAlign: string = "start"`, `columnCount: number = 0`, `fullWidth: boolean = false`, `openOnHover: boolean = true`, `activeCategory: string = ""`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = "icon-[lucide--arrow-right]"`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
- Slots: `default`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`, `category({ item: object; value: string; index: number })`
### Nav
@@ -826,8 +826,8 @@ Theme-aware, responsive nav component.
Theme-aware, responsive navbar component.
- Mount: `data-component="Navbar"`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
- Slots: `topbar`, `actions`
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `floating: boolean = false`, `startFloating: string = "0px"`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `collapseAt: string = "900px"`, `mobilePanel: string = "content"`, `mobileUtilitiesPosition: string = "header"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `translationPrefix: string = ""`, `class: string = ""`
- Slots: `topbar`, `actions`, `utilities`
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })`
### Pagination
@@ -844,7 +844,7 @@ Theme-aware, responsive pagination component.
Theme-aware, responsive scrollspy component.
- Mount: `data-component="Scrollspy"`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `class: string = ""`
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `sticky: boolean = false`, `topOffset: string = "6rem"`, `sectionOffset: string = "7rem"`, `collapseAt: string = "800px"`, `class: string = ""`
- Slots: `default`
- Outputs: `change({ href: string; label: string })`
+56
View File
@@ -90,6 +90,62 @@ the component directory at runtime.
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
equivalent is exported as `@wrnexus/ui/component-reference.json`.
### Typed Navbar items
Keep navigation data in a shared TypeScript module and validate it with the
package contract. `NavbarItem` is a discriminated union of `link`, `dropdown`,
and `mega`; a mega item accepts every `MegaMenu` layout option.
```ts
import type { NavbarItem } from "@wrnexus/ui";
export const navigation = [
{ type: "link", label: "Pricing", href: "/pricing" },
{
type: "dropdown",
label: "Company",
items: [
{ label: "About", href: "/about" },
{ label: "Contact", href: "/contact" },
],
},
{
type: "mega",
label: "Products",
mega: {
variant: "icon-grid",
panelWidth: "2xl",
columnCount: 3,
rail: [{ label: "All products", href: "/products" }],
columns: [
{
heading: "Work",
items: [
{
label: "Projects",
href: "/products/projects",
icon: "icon-[lucide--folders]",
description: "Plan and deliver work",
badge: "New",
},
],
},
],
featured: {
title: "What's new",
actionLabel: "Explore",
actionHref: "/new",
},
actionLabel: "View all products",
actionHref: "/products",
},
},
] satisfies NavbarItem[];
```
Importing from `@wrnexus/ui/navigation` is also supported. Existing untyped
Navbar data using `children` remains accepted at runtime for migration.
## API
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to

Some files were not shown because too many files have changed in this diff Show More