Compare commits

...
80 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
Clintchiz 631e1a2ddb test: cover client-created component branches
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 11:54:28 +05:30
Clintchiz 2e12656060 feat: close application architecture gaps
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 11:45:57 +05:30
ClintchizandClaude Opus 5 d87b197224 fix(queue): let a shut-down queue be started again
`shutdown()` set `accepting = false` and `start()` refused for ever after, so
a queue was single-use. Any process that boots more than one app broke: a test
suite closing one harness and opening the next, a hot reload, a multi-tenant
host. The failure landed far from its cause -- the SECOND app to boot threw
WRN-QUEUE-CLOSED out of the dev server because an unrelated one had shut down
earlier in the same process. That is what turned six example-app security
tests red only when run alongside the rest of the suite.

Starting is an explicit intent to run, so it reopens the queue. `add()` keeps
its guard, so work offered to a queue that is shutting down is still refused.

Also migrates the example app's welcome-email queue to `defineQueue`, which
the new loader requires. It still used `defineJob`, so the loader refused it
and took the whole example app down -- 14 failures from one unmigrated file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 11:36:44 +05:30
Clintchiz 64ab20cc95 feat: add application productivity foundations
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 11:13:03 +05:30
Clintchiz 46195462c3 feat: make queues durable by default and add seed helpers
Quality / quality (ubuntu-latest) (push) Failing after 11m1s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 09:50:25 +05:30
Clintchiz 8fb96f521f fix(cli): resolve runtime config import absolutely
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 09:29:26 +05:30
Clintchiz 9377a69d88 feat(queue): add typed application queue lifecycle
Quality / quality (ubuntu-latest) (push) Failing after 10m55s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-23 09:22:05 +05:30
Clintchiz 9332522614 fix: initialize configured auth in production builds
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:36:31 +05:30
Clintchiz 37439188ca fix: share authz catalog across package instances
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:30:01 +05:30
Clintchiz 7c5069b5b5 fix: honor project typecheck in canonical checks
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:23:21 +05:30
Clintchiz 41b7eb8798 fix: keep authz server modules out of browser bundles
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:20:06 +05:30
Clintchiz 0be69fb234 fix: avoid auth plugin ordering cycles
Quality / quality (ubuntu-latest) (push) Failing after 10m56s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:17:33 +05:30
Clintchiz bab11083c6 fix: ship authz runtime and preserve queue clocks
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:14:26 +05:30
199 changed files with 10001 additions and 614 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
+139 -105
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.15",
"version": "0.8.18",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*",
@@ -257,7 +257,7 @@
},
"packages/authz": {
"name": "@wrnexus/authz",
"version": "0.8.10",
"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.49",
"version": "0.8.64",
"bin": {
"wrnexus": "src/index.ts",
},
@@ -320,7 +320,7 @@
},
"packages/compiler": {
"name": "@wrnexus/compiler",
"version": "0.8.17",
"version": "0.8.22",
"dependencies": {
"@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*",
@@ -342,14 +342,14 @@
},
"packages/csr": {
"name": "@wrnexus/csr",
"version": "0.8.28",
"version": "0.8.35",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
},
"packages/db": {
"name": "@wrnexus/db",
"version": "0.8.18",
"version": "0.8.20",
"devDependencies": {
"@types/bun": "^1.3.14",
"typescript": "^6.0.3",
@@ -357,7 +357,7 @@
},
"packages/dev-server": {
"name": "@wrnexus/dev-server",
"version": "0.8.45",
"version": "0.8.56",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*",
@@ -371,6 +371,8 @@
"@wrnexus/plugin": "workspace:*",
"@wrnexus/pubsub": "workspace:*",
"@wrnexus/pwa": "workspace:*",
"@wrnexus/queue": "workspace:*",
"@wrnexus/react": "workspace:*",
"@wrnexus/router": "workspace:*",
"@wrnexus/rpc": "workspace:*",
"@wrnexus/security": "workspace:*",
@@ -384,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",
@@ -392,9 +394,10 @@
},
"packages/encryption": {
"name": "@wrnexus/encryption",
"version": "0.8.9",
"version": "0.8.10",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
},
"devDependencies": {
"@types/bun": "^1.3.14",
@@ -410,9 +413,11 @@
},
"packages/helpers": {
"name": "@wrnexus/helpers",
"version": "0.8.8",
"version": "0.8.9",
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
},
},
"packages/i18n": {
@@ -481,6 +486,14 @@
"vscode-html-languageservice": "^5.6.2",
},
},
"packages/mail": {
"name": "@wrnexus/mail",
"version": "0.8.3",
"dependencies": {
"@wrnexus/encryption": "workspace:*",
"@wrnexus/queue": "workspace:*",
},
},
"packages/mcp": {
"name": "@wrnexus/mcp",
"version": "0.8.8",
@@ -488,6 +501,10 @@
"wrnexus-mcp": "src/stdio.ts",
},
},
"packages/metering": {
"name": "@wrnexus/metering",
"version": "0.8.3",
},
"packages/mobile": {
"name": "@wrnexus/mobile",
"version": "0.8.8",
@@ -513,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",
@@ -546,9 +571,10 @@
},
"packages/queue": {
"name": "@wrnexus/queue",
"version": "0.8.9",
"version": "0.8.14",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/rpc": "workspace:*",
},
},
@@ -573,7 +599,7 @@
},
"packages/reactive": {
"name": "@wrnexus/reactive",
"version": "0.8.8",
"version": "0.8.9",
},
"packages/realtime": {
"name": "@wrnexus/realtime",
@@ -591,7 +617,7 @@
},
"packages/router": {
"name": "@wrnexus/router",
"version": "0.8.9",
"version": "0.8.10",
"dependencies": {
"@wrnexus/compiler": "workspace:*",
"@wrnexus/core": "workspace:*",
@@ -629,11 +655,11 @@
},
"packages/store": {
"name": "@wrnexus/store",
"version": "0.8.9",
"version": "0.8.12",
},
"packages/styles": {
"name": "@wrnexus/styles",
"version": "0.8.17",
"version": "0.8.20",
"dependencies": {
"@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*",
@@ -642,11 +668,11 @@
},
"packages/syntax": {
"name": "@wrnexus/syntax",
"version": "0.8.13",
"version": "0.8.18",
},
"packages/test": {
"name": "@wrnexus/test",
"version": "0.8.10",
"version": "0.8.12",
},
"packages/tracking": {
"name": "@wrnexus/tracking",
@@ -654,7 +680,7 @@
},
"packages/typecheck": {
"name": "@wrnexus/typecheck",
"version": "0.8.11",
"version": "0.8.16",
"dependencies": {
"@wrnexus/syntax": "workspace:*",
"typescript": "^6.0.3",
@@ -662,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:*",
@@ -770,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=="],
@@ -796,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=="],
@@ -816,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=="],
@@ -846,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=="],
@@ -950,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=="],
@@ -958,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=="],
@@ -1034,10 +1062,14 @@
"@wrnexus/language-server": ["@wrnexus/language-server@workspace:packages/language-server"],
"@wrnexus/mail": ["@wrnexus/mail@workspace:packages/mail"],
"@wrnexus/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"],
"@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"],
"@wrnexus/metering": ["@wrnexus/metering@workspace:packages/metering"],
"@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"],
"@wrnexus/native": ["@wrnexus/native@workspace:packages/native"],
@@ -1046,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"],
@@ -1088,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=="],
@@ -1114,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=="],
@@ -1146,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=="],
@@ -1168,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=="],
@@ -1204,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=="],
@@ -1212,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"],
@@ -1284,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=="],
@@ -1320,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=="],
@@ -1338,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=="],
@@ -1350,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=="],
@@ -1428,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=="],
+43
View File
@@ -0,0 +1,43 @@
# Application productivity kit
WrNexus applications should declare infrastructure once and keep only domain decisions locally.
## Included foundations
1. HTTP routes: `defineApiRoute`, `authorized`, `requireUser`, `requirePermission`, `requireParam`, `requireJson`, and `json`.
2. CRUD resources: `defineResource` generates owned list/create/get/update/delete handlers.
3. Seed data: `addSeedData`, `upsertSeedData`, `seedIfMissing`, `defineSeed`, `removeSeedData`, and `runSeedQuery`.
4. Transactional delivery: `withOutbox`, `drainOutbox`, and `installOutboxSchema`.
5. Job state transitions: `defineJobStateMachine`.
6. SQLite/PostgreSQL/MySQL queue tables: configured storage plus `databaseQueueStore`.
7. Queue operations: success/failed hooks, dashboard handler and queue CLI.
8. Queue testing: `testQueue` with deterministic `runNext`/`runAll`.
9. Workflows and compensation: durable workflows and `defineSaga`.
10. Mail: `defineMail`, `defineMailTemplate`, sandbox allowlists and `queuedMail`.
11. Encrypted settings: `defineSecretResource`.
12. Ledgers: `defineLedger` with atomic debit and audit entries.
13. Forms: `useForm` for values, errors, dirty/submitting state, validation and submission.
14. Typed HTTP calls: `createApiClient`; generated OpenAPI/SDK clients remain available through `wrnexus api generate`.
15. Full-stack testing: `createTestApp`, database rollback/factories, and queue test controls.
16. Package migrations: queue/auth/authz packages contribute migrations through the plugin lifecycle.
17. Auth/OAuth: auth and authz plugins own routes, middleware, roles and provider lifecycle.
18. Code generation: `wrnexus generate resource <name>`.
19. Incremental features: `wrnexus add queue|mail|seed|testing|resource`.
20. Starter presets: `wrnexus create app --preset=saas|email` and `--features=...`.
## Recommended new-project flow
```sh
wrnexus create acme --preset=saas
cd acme && bun install
wrnexus generate resource projects
wrnexus add mail
wrnexus db migrate
wrnexus db seed
wrnexus dev
```
Use generated resources for ordinary owned CRUD. Use the outbox for business operations that
change data and enqueue work together. Use sagas only when a workflow crosses systems that cannot
share a transaction. Application code should retain product-specific policy; framework code owns
storage, validation, lifecycle, retries, diagnostics and scaffolding.
@@ -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
+195 -8
View File
@@ -472,8 +472,6 @@
"AuthzAuditEvent",
"AuthzAuditSink",
"AuthzCatalog",
"AuthzConfig",
"AuthzIdentityEvent",
"AuthzModule",
"AuthzResolver",
"AuthzResolverOptions",
@@ -499,12 +497,10 @@
"allow",
"any",
"anyDecision",
"assignDefaultAuthzRoles",
"attr",
"authorize",
"authorizeDecision",
"authzMiddleware",
"authzPlugin",
"cachedPermissionStore",
"can",
"consoleAuditSink",
@@ -536,18 +532,29 @@
"requireRole",
"safeRecord",
"scopeKey",
"setAuthzCatalog",
"setDefaultAuthzRoles"
"setAuthzCatalog"
],
"./db": [
"authzMigrationSql",
"dbPermissionStore",
"ensureAuthzTables"
],
"./defaults": [
"AuthzIdentityEvent",
"assignDefaultAuthzRoles",
"setDefaultAuthzRoleStore",
"setDefaultAuthzRoles"
],
"./plugin": [
"AuthzConfig",
"authzPlugin",
"default"
],
"./runtime-memory-middleware": [
"default"
],
"./runtime-middleware": [
"default"
]
},
"@wrnexus/benchmark": {
@@ -961,6 +968,7 @@
},
"@wrnexus/cli": {
".": [],
"./test-setup": [],
"./workspace": [
"ResolvedWorkspaceApp",
"ResolvedWorkspaceConfig",
@@ -1345,6 +1353,8 @@
"ActionClientError",
"ActionClientOptions",
"ActionResult",
"ApiClientOptions",
"ApiRequest",
"ClientModuleScope",
"HydrationScopeApi",
"NAV_RUNTIME",
@@ -1358,6 +1368,7 @@
"callServerFunction",
"collectRefs",
"createActionClient",
"createApiClient",
"createOutputProxy",
"createServerProxy",
"getActionRuntime",
@@ -1368,11 +1379,13 @@
"invalidateClientModule",
"invokeOutput",
"loadClientFunctions",
"registerOutputHandler"
"registerOutputHandler",
"useFetch"
]
},
"@wrnexus/db": {
".": [
"AddSeedOptions",
"BaseType",
"Column",
"ColumnDef",
@@ -1383,6 +1396,7 @@
"Dialect",
"Driver",
"ExecResult",
"LedgerOptions",
"Migration",
"MigrationRunOptions",
"MigrationSafetyIssue",
@@ -1399,7 +1413,13 @@
"RelationOptions",
"Repository",
"Row",
"SeedDatabase",
"SeedRow",
"SeedStep",
"SeedUserAccount",
"TxHandle",
"UpsertSeedOptions",
"addSeedData",
"analyzeMigrationSafety",
"analyzeMigrations",
"appliedMigrations",
@@ -1414,6 +1434,8 @@
"cursorPaginate",
"databaseHealth",
"databaseNames",
"defineLedger",
"defineSeed",
"exists",
"firstOrThrow",
"generateQueriesFile",
@@ -1431,15 +1453,20 @@
"queryOperation",
"registerDb",
"registerLazyDb",
"removeSeedData",
"resetDbPerformanceSnapshot",
"retryTransaction",
"rollback",
"runSeedQuery",
"scaffoldMigration",
"seedIfMissing",
"seedUsers",
"setDb",
"softDeleteClause",
"status",
"table",
"tenantScope",
"upsertSeedData",
"v",
"withTransaction"
],
@@ -1640,6 +1667,7 @@
"EncryptionKey",
"EncryptionKeyring",
"ReplayStore",
"SecretResourceOptions",
"createEncryptedRequest",
"createKeyring",
"createMemoryReplayStore",
@@ -1647,6 +1675,7 @@
"decryptEncryptedResponse",
"decryptHttpBody",
"decryptRequest",
"defineSecretResource",
"deriveKey",
"encrypt",
"encryptHttpBody",
@@ -1680,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"
@@ -1895,6 +1937,25 @@
"isApiReferenceAt"
]
},
"@wrnexus/mail": {
".": [
"MailDriver",
"MailMessage",
"MailOptions",
"MailSandboxDecision",
"MailTemplate",
"SealedMailCredentialsOptions",
"SealedMailDriverOptions",
"defineMail",
"defineMailTemplate",
"defineSealedMailCredentials",
"interpolateTemplate",
"mailSandboxDecision",
"queuedMail",
"sealedMailDriver",
"storedMailTemplate"
]
},
"@wrnexus/mcp": {
".": [
"McpServer",
@@ -1906,6 +1967,20 @@
"runMcpStdio"
]
},
"@wrnexus/metering": {
".": [
"EntitlementPlan",
"EntitlementsOptions",
"MeterKind",
"MeterOptions",
"MeterPack",
"MeterResult",
"MeterStore",
"defineEntitlements",
"defineMeter",
"definePacks"
]
},
"@wrnexus/mobile": {
".": [
"CapacitorBridge",
@@ -2106,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",
@@ -2270,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",
@@ -2296,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": {
@@ -2356,6 +2537,7 @@
".": [
"AnimationTimeline",
"Cleanup",
"FormState",
"HistorySignal",
"ReactiveContext",
"ReactiveScope",
@@ -2382,6 +2564,7 @@
"transition",
"untrack",
"urlSignal",
"useForm",
"watch"
]
},
@@ -2909,11 +3092,14 @@
".": [
"BrowserArtifactPage",
"Deferred",
"DetectMutationsOptions",
"FetchMock",
"Harness",
"HarnessOptions",
"JsonResponse",
"MemoryCookieJar",
"MutationCase",
"MutationReport",
"TestRequestOptions",
"TransactionalDatabase",
"WaitForOptions",
@@ -2931,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
+145 -8
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;
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"}]
+19 -4
View File
@@ -1,13 +1,28 @@
import { defineJob } from "@wrnexus/queue";
import { defineQueue } from "@wrnexus/queue";
export interface WelcomeEmailPayload {
userId: number;
email: string;
}
export default defineJob<WelcomeEmailPayload>({
/*
* A file under app/queues must default-export defineQueue(...) -- the loader
* refuses anything else rather than registering a queue that would never run.
* This example used the older defineJob() shape and so failed to load, which
* took the whole example app down with it.
*/
export default defineQueue({
name: "welcome-email",
async run(job) {
console.log(`Welcome email queued for ${job.data.email}`);
jobs: {
send: {
validate: (data: unknown): data is WelcomeEmailPayload =>
typeof data === "object" &&
data !== null &&
Number.isInteger((data as { userId?: unknown }).userId) &&
typeof (data as { email?: unknown }).email === "string",
run: async ({ email }: WelcomeEmailPayload) => {
console.log(`Welcome email queued for ${email}`);
},
},
},
});
+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.15",
"version": "0.8.18",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module",
"sideEffects": false,
+10
View File
@@ -11,6 +11,7 @@ import {
import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts";
import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts";
import { evaluateAuthRisk } from "./risk.ts";
import { setDefaultAuthEngine } from "./runtime.ts";
import type { AuthStore } from "./store.ts";
import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts";
import type {
@@ -1671,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();
@@ -2298,5 +2304,9 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
},
};
// The application config is evaluated in both dev and generated production
// runtimes. Registering here makes that configured engine available to the
// package-owned routes without an application startup shim.
setDefaultAuthEngine(engine);
return engine;
}
+1 -1
View File
@@ -11,7 +11,7 @@ import {
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
import type { AuthSessionVerificationHandler } from "../types.ts";
import { completeAuth, startAuth, type OAuthProvider } from "@wrnexus/oauth";
import { assignDefaultAuthzRoles } from "@wrnexus/authz";
import { assignDefaultAuthzRoles } from "@wrnexus/authz/defaults";
function text(value: unknown): string {
return typeof value === "string" ? value : value == null ? "" : String(value);
+5 -2
View File
@@ -1,13 +1,16 @@
{
"name": "@wrnexus/authz",
"version": "0.8.10",
"version": "0.8.15",
"private": true,
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts",
"./db": "./src/db.ts",
"./plugin": "./src/plugin.ts"
"./plugin": "./src/plugin.ts",
"./defaults": "./src/defaults.ts",
"./runtime-middleware": "./src/runtime-middleware.ts",
"./runtime-memory-middleware": "./src/runtime-memory-middleware.ts"
},
"dependencies": {
"@wrnexus/core": "workspace:*",
+13 -3
View File
@@ -32,16 +32,26 @@
import type { AuthzCatalog } from "./types.ts";
let catalog: AuthzCatalog | undefined;
interface AuthzCatalogState {
catalog?: AuthzCatalog;
}
const catalogKey = Symbol.for("@wrnexus/authz:catalog:v1");
function state(): AuthzCatalogState {
const global = globalThis as Record<PropertyKey, unknown>;
return (global[catalogKey] ??= {}) as AuthzCatalogState;
}
/** Set the process-wide authorization catalog (called by the framework at boot). */
export function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog {
catalog = next;
state().catalog = next;
return next;
}
/** The process-wide authorization catalog. Throws if it hasn't been set. */
export function getAuthzCatalog(): AuthzCatalog {
const catalog = state().catalog;
if (!catalog) {
throw new Error(
"WRN-AUTHZ-SETUP: no authorization catalog is configured. The dev server and " +
@@ -59,5 +69,5 @@ export function getAuthzCatalog(): AuthzCatalog {
/** Whether the process-wide authorization catalog has been set. */
export function hasAuthzCatalog(): boolean {
return catalog !== undefined;
return state().catalog !== undefined;
}
-4
View File
@@ -172,7 +172,3 @@ export type {
} from "./types.ts";
export type { AuthorizeDecisionOptions } from "./advanced.ts";
export { generatePermissionTypes } from "./codegen.ts";
export { authzPlugin } from "./plugin.ts";
export type { AuthzConfig } from "./plugin.ts";
export { assignDefaultAuthzRoles, setDefaultAuthzRoles } from "./defaults.ts";
export type { AuthzIdentityEvent } from "./defaults.ts";
-1
View File
@@ -51,7 +51,6 @@ export function authzPlugin() {
return definePlugin({
name: "@wrnexus/authz",
version: "0.8.0",
after: ["@wrnexus/auth"],
configure(config, context) {
const raw = (config.authz ?? {}) as AuthzConfig;
const enabled = raw.enabled !== false && Boolean(config.authz);
+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.49",
"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"
+35
View File
@@ -0,0 +1,35 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { runGenerateResource } from "./generate.ts";
const FEATURES: Record<string, Record<string, string>> = {
queue: {
"app/queues/example.ts": `import { defineQueue } from "@wrnexus/queue";\n\nexport default defineQueue({\n name: "example",\n jobs: {\n run: {\n run: async (data: { id: string }) => { console.log("job", data.id); },\n success: async (data) => { console.log("completed", data.id); },\n failed: async (data, error) => { console.error("failed", data.id, error); },\n },\n },\n});\n`,
},
mail: {
"app/lib/mail.ts": `import { defineMail } from "@wrnexus/mail";\n\nexport const mail = defineMail({\n from: process.env.MAIL_FROM,\n driver: { send: async () => { throw new Error("Configure a mail driver"); } },\n sandbox: { enabled: process.env.NODE_ENV !== "production", allowlist: [] },\n});\n`,
},
seed: {
"app/db/seed.ts": `import { addSeedData, defineSeed } from "@wrnexus/db";\n\nexport default defineSeed(async (db) => {\n await addSeedData(db, { name: "Example" }, "examples", { conflict: "ignore" });\n});\n`,
},
testing: {
"test/app.test.ts": `import { expect, test } from "bun:test";\nimport { createTestApp } from "@wrnexus/test";\n\ntest("app boots", async () => {\n const app = await createTestApp(import.meta.dir + "/..");\n expect((await app.fetch("/healthz")).ok).toBe(true);\n app.close();\n});\n`,
},
};
export function runAdd(root: string, feature: string | undefined, name?: string) {
if (feature === "resource") return runGenerateResource(root, name);
const files = feature ? FEATURES[feature] : undefined;
if (!files)
throw new Error(`Use wrnexus add ${Object.keys(FEATURES).join(" | ")} | resource <name>`);
for (const [relative, content] of Object.entries(files)) {
const target = join(resolve(root), relative);
if (existsSync(target)) {
console.log(`${relative} exists — skipped`);
continue;
}
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, content, "utf8");
console.log(`✓ Added ${relative}`);
}
}
+20
View File
@@ -702,6 +702,15 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
imports.push(`import "./.authz-setup.ts";`);
}
// Runtime plugin state (notably the configured auth engine and OAuth
// providers) is established by evaluating the application config. The build
// process runs in a different process, so its globals cannot serve the
// generated production server.
const runtimeConfigPath = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]
.map((name) => resolve(appRoot, name))
.find(existsSync);
if (runtimeConfigPath) imports.push(`import ${JSON.stringify(fwd(runtimeConfigPath))};`);
const manifestRoutes = (routes: Route[]): string => {
const parts = routes.map((r) => {
const v = `m${counter++}`;
@@ -754,6 +763,15 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
.join(", ");
if (router.services.length) console.log(`✓ RPC services: ${router.services.length}`);
const queuesLit = (router.queues ?? [])
.map((queue) => {
const v = `q${counter++}`;
imports.push(`import * as ${v} from ${JSON.stringify(fwd(queue.file))};`);
return `{ name: ${JSON.stringify(queue.name)}, mod: ${v} }`;
})
.join(", ");
if (router.queues?.length) console.log(`✓ Queues: ${router.queues.length}`);
// Authorization declarations again, this time for ProdOptions.authz — a
// SEPARATE set of static imports of the exact same files (harmless; ES
// modules are evaluated once and shared across every importer), statically
@@ -789,6 +807,7 @@ await createProductionServer(
components: [${componentsLit}],
layouts: [${layoutsLit}],
services: [${servicesLit}],
queues: [${queuesLit}],
},
{
reactivePath: join(import.meta.dir, "reactive.js"),
@@ -806,6 +825,7 @@ await createProductionServer(
i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
db: ${config.db ? JSON.stringify(config.db) : "undefined"},
databases: ${config.databases ? JSON.stringify(config.databases) : "undefined"},
queue: ${config.queue ? JSON.stringify(config.queue) : "undefined"},
storage: ${config.storage ? JSON.stringify(config.storage) : "undefined"},
${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""}
${
+4 -1
View File
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { runBuild } from "./build.ts";
import { runTypecheck } from "./types.ts";
import { assertReachability } from "./reachability.ts";
async function runScript(root: string, name: string): Promise<void> {
const child = Bun.spawn(["bun", "run", name], {
@@ -20,8 +21,10 @@ export async function runCheck(appRoot: string): Promise<void> {
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")) as {
scripts?: Record<string, string>;
};
assertReachability(root);
await runBuild(root);
if (!(await runTypecheck(root))) throw new Error("WRN-CHECK: application typecheck failed");
if (manifest.scripts?.typecheck) await runScript(root, "typecheck");
else if (!(await runTypecheck(root))) throw new Error("WRN-CHECK: application typecheck failed");
for (const name of ["lint", "test", "format:check"] as const) {
if (manifest.scripts?.[name]) await runScript(root, name);
}
+16 -2
View File
@@ -7,6 +7,7 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { runAdd } from "./add.ts";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
import { currentCliVersion } from "./update-notifier.ts";
@@ -672,13 +673,26 @@ export function scaffoldApp(root: string, appName: string): void {
}
}
export function createApp(name: string): void {
export function createApp(name: string, options: readonly string[] = []): void {
if (!name) {
console.error("Usage: wrnexus create <app-name>");
process.exit(1);
}
scaffoldApp(resolve(process.cwd(), name), name);
const root = resolve(process.cwd(), name);
scaffoldApp(root, name);
const preset = options.find((value) => value.startsWith("--preset="))?.split("=")[1];
const requested =
options
.find((value) => value.startsWith("--features="))
?.split("=")[1]
?.split(",") ?? [];
const features = new Set([
...(preset === "saas" ? ["queue", "mail", "seed", "testing"] : []),
...(preset === "email" ? ["queue", "mail", "testing"] : []),
...requested,
]);
for (const feature of features) runAdd(root, feature);
console.log(`✓ Created ${name}`);
console.log(`\nNext steps:`);
+40
View File
@@ -30,6 +30,46 @@ export interface GeneratedFile {
content: string;
}
export function scaffoldResource(name: string): GeneratedFile[] {
const clean = name.replace(/[^A-Za-z0-9_-]/g, "").toLowerCase();
if (!clean) throw new TypeError("resource name is required");
const singular = clean.replace(/s$/, "");
const pascal = toPascalCase(singular);
return [
{
path: `schemas/${singular}.ts`,
content: `import { v } from "@wrnexus/validation";\n\nexport default v.object({\n name: v.string().min(1, "Required"),\n});\n`,
},
{
path: `resources/${clean}.ts`,
content: `import { getDb } from "@wrnexus/db";\nimport { defineResource } from "@wrnexus/helpers";\n\nexport default defineResource({\n name: "${clean}",\n db: getDb,\n table: "${clean}",\n owner: "owner_id",\n fields: ["name"],\n permissions: { read: "${singular}:read", write: "${singular}:write" },\n});\n`,
},
{
path: `api/${clean}.ts`,
content: `import resource from "../resources/${clean}.ts";\nexport const GET = resource.list;\nexport const POST = resource.create;\n`,
},
{
path: `api/${clean}/[id].ts`,
content: `import resource from "../../resources/${clean}.ts";\nexport const GET = resource.get;\nexport const PUT = resource.update;\nexport const DELETE = resource.remove;\n`,
},
{
path: `pages/${clean}.wrn`,
content: `page ${pascal}List {\n view {\n <h1>${pascal}</h1>\n <p>Generated resource page. Connect it to /api/${clean}.</p>\n }\n}\n`,
},
];
}
export function runGenerateResource(appRoot: string, name: string | undefined): void {
if (!name) throw new TypeError("Usage: wrnexus generate resource <name>");
for (const file of scaffoldResource(name)) {
const target = join(resolve(appRoot), "app", file.path);
if (existsSync(target)) continue;
mkdirSync(dirname(target), { recursive: true });
writeFileSync(target, file.content, "utf8");
console.log(`✓ Generated app/${file.path}`);
}
}
function toPascalCase(name: string): string {
return name
.split(/[^A-Za-z0-9]+/)
+26 -1
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) {
@@ -167,7 +172,7 @@ async function main(): Promise<void> {
break;
}
case "create":
createApp(rest[0] ?? "");
createApp(rest.find((value) => !value.startsWith("--")) ?? "", rest);
break;
case "workspace": {
const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts");
@@ -192,6 +197,11 @@ async function main(): Promise<void> {
}
case "generate":
case "g": {
if (rest[0] === "resource") {
const { runGenerateResource } = await import("./generate.ts");
runGenerateResource(".", rest[1]);
break;
}
if (rest[0] === "routes") {
const { regenerateRoutes } = await import("./routes.ts");
const n = regenerateRoutes(join(resolve(rest[1] ?? "."), "app"));
@@ -307,6 +317,21 @@ async function main(): Promise<void> {
if (!result.ok) process.exitCode = 1;
break;
}
case "queue": {
const values = rest.filter((value) => !value.startsWith("--"));
const { runQueueCommand } = await import("./queue-command.ts");
await runQueueCommand(values[2] ?? ".", values[0] ?? "status", [
values[1] ?? "",
...rest.filter((value) => value.startsWith("--")),
]);
break;
}
case "add": {
const values = rest.filter((value) => !value.startsWith("--"));
const { runAdd } = await import("./add.ts");
runAdd(values[2] ?? ".", values[0], values[1]);
break;
}
case "security": {
const values = rest.filter((value) => !value.startsWith("--"));
const { runSecurityCommand } = await import("./security-command.ts");
+86
View File
@@ -0,0 +1,86 @@
import { resolve } from "node:path";
import { loadAppConfig } from "@wrnexus/styles";
import { connectFromConfig } from "@wrnexus/db/connect";
import { installSqliteQueueSchema } from "@wrnexus/queue";
export async function runQueueCommand(root: string, command: string, args: readonly string[]) {
const appRoot = resolve(root);
const config = await loadAppConfig(appRoot, process.env.WRNEXUS_PROFILE);
const selected = config.queue?.databaseName ?? "default";
const dbConfig = selected === "default" ? config.db : config.databases?.[selected];
const effective =
config.queue?.storage === "database"
? dbConfig
: { driver: "sqlite" as const, url: "file:./.wrnexus/queue.sqlite" };
if (!effective || effective.driver === "mongo")
throw new Error(`Queue database '${selected}' is not configured as SQL.`);
const db = connectFromConfig(effective, appRoot);
if (db.driver.dialect !== "sqlite")
throw new Error("Queue CLI currently requires SQLite configured storage.");
const table = config.queue?.table ?? "wrnexus_jobs";
await installSqliteQueueSchema(db, table);
try {
const id = args.find((value) => !value.startsWith("--"));
if (command === "list") {
console.table(
await db.all(
`SELECT id,name,run_at,priority,lease_owner,lease_until FROM ${table} ORDER BY run_at`,
),
);
return;
}
if (command === "status") {
const pending = await db.one<{ total: number }>(`SELECT COUNT(*) AS total FROM ${table}`);
const history = await db.one<{ total: number }>(
`SELECT COUNT(*) AS total FROM ${table}_history`,
);
console.log(
JSON.stringify(
{ pending: Number(pending?.total ?? 0), history: Number(history?.total ?? 0) },
null,
2,
),
);
return;
}
if (!id && command !== "clear") throw new Error(`wrnexus queue ${command} requires a job id`);
if (command === "inspect") {
console.log(
JSON.stringify(await db.one(`SELECT * FROM ${table} WHERE id = ?`, [id]), null, 2),
);
return;
}
if (command === "cancel") {
await db.exec(`DELETE FROM ${table} WHERE id = ?`, [id]);
return;
}
if (command === "retry") {
const row = await db.one<{ payload: string }>(
`SELECT payload FROM ${table}_history WHERE id = ? AND state = 'failed'`,
[id],
);
if (!row) throw new Error(`Failed job '${id}' was not found.`);
const job = JSON.parse(row.payload) as {
name: string;
runAt: number;
priority: number;
idempotencyKey?: string;
};
job.runAt = Date.now();
await db.exec(
`INSERT INTO ${table} (id,name,payload,run_at,priority,idempotency_key) VALUES (?,?,?,?,?,?)`,
[id, job.name, JSON.stringify(job), job.runAt, job.priority, job.idempotencyKey ?? null],
);
return;
}
if (command === "clear" && args.includes("--completed")) {
await db.exec(`DELETE FROM ${table}_history WHERE state = 'completed'`);
return;
}
throw new Error(
"Use queue list | status | inspect <id> | retry <id> | cancel <id> | clear --completed",
);
} finally {
await db.close();
}
}
+80
View File
@@ -0,0 +1,80 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { dirname, extname, join, relative, resolve } from "node:path";
export interface ReachabilityIssue {
code: "WRN-REACH-MIDDLEWARE" | "WRN-REACH-QUEUE" | "WRN-REACH-WORKER";
file: string;
message: string;
}
function sourceFiles(directory: string): string[] {
if (!existsSync(directory)) return [];
const output: string[] = [];
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) output.push(...sourceFiles(path));
else if ([".ts", ".js", ".mts", ".mjs"].includes(extname(entry.name))) output.push(path);
}
return output;
}
/** Static registration audit for convention-discovered application modules. */
export function checkReachability(appRoot: string): ReachabilityIssue[] {
const root = resolve(appRoot);
const app = join(root, "app");
const all = sourceFiles(app).map((file) => ({ file, source: readFileSync(file, "utf8") }));
const issues: ReachabilityIssue[] = [];
for (const entry of all) {
const path = relative(root, entry.file).replace(/\\/g, "/");
if (path.startsWith("app/middleware/") && !/\bexport\s+default\b/.test(entry.source)) {
issues.push({
code: "WRN-REACH-MIDDLEWARE",
file: path,
message: "Middleware discovery requires a default export.",
});
}
if (
path.startsWith("app/queues/") &&
/\bdefineQueue\s*\(/.test(entry.source) &&
!/\bexport\s+default\b/.test(entry.source)
) {
issues.push({
code: "WRN-REACH-QUEUE",
file: path,
message: "Queue discovery requires the defined queue to be exported as default.",
});
}
if (
/\bdefineWorker\s*(?:<[^>]+>)?\s*\(/.test(entry.source) &&
!/\brunWorker\s*\(/.test(entry.source)
) {
const stem = entry.file.replace(/\.[^.]+$/, "");
const imported = all.some(
(candidate) =>
candidate.file !== entry.file &&
candidate.source
.replace(/\\/g, "/")
.includes(relative(dirname(candidate.file), stem).replace(/\\/g, "/")),
);
if (!imported) {
issues.push({
code: "WRN-REACH-WORKER",
file: path,
message: "Worker is defined but no application module imports it or calls runWorker().",
});
}
}
}
return issues;
}
export function assertReachability(appRoot: string): void {
const issues = checkReachability(appRoot);
if (!issues.length) return;
throw new Error(
`WRN-REACHABILITY: unreachable application exports\n${issues
.map((issue) => `- ${issue.code} ${issue.file}: ${issue.message}`)
.join("\n")}`,
);
}
+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(
+13 -1
View File
@@ -1,5 +1,5 @@
import { test, expect } from "bun:test";
import { scaffold } from "../src/generate.ts";
import { scaffold, scaffoldResource } from "../src/generate.ts";
test("scaffold page: kebab file, PascalCase page name", () => {
const f = scaffold("page", "about-us");
@@ -32,3 +32,15 @@ test("scaffold strips extensions from the given name", () => {
expect(scaffold("page", "home.wrn").path).toBe("pages/home.wrn");
expect(scaffold("api", "ping.ts").path).toBe("api/ping.ts");
});
test("resource scaffold emits schema, handlers, routes and a page", () => {
const files = scaffoldResource("templates");
expect(files.map((file) => file.path)).toEqual([
"schemas/template.ts",
"resources/templates.ts",
"api/templates.ts",
"api/templates/[id].ts",
"pages/templates.wrn",
]);
expect(files[1]?.content).toContain("defineResource");
});
+31
View File
@@ -0,0 +1,31 @@
import { expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { checkReachability } from "../src/reachability.ts";
test("reports convention modules and workers that cannot be reached", () => {
const root = mkdtempSync(join(tmpdir(), "wrn-reach-"));
mkdirSync(join(root, "app", "middleware"), { recursive: true });
mkdirSync(join(root, "app", "queues"), { recursive: true });
writeFileSync(join(root, "app", "middleware", "audit.ts"), "export const audit = () => {};\n");
writeFileSync(
join(root, "app", "queues", "mail.ts"),
"export const mail = defineQueue({}); export const worker = defineWorker({});\n",
);
expect(
checkReachability(root)
.map((issue) => issue.code)
.sort(),
).toEqual(["WRN-REACH-MIDDLEWARE", "WRN-REACH-QUEUE", "WRN-REACH-WORKER"]);
});
test("accepts discovered middleware and queues", () => {
const root = mkdtempSync(join(tmpdir(), "wrn-reach-"));
mkdirSync(join(root, "app", "middleware"), { recursive: true });
mkdirSync(join(root, "app", "queues"), { recursive: true });
writeFileSync(join(root, "app", "middleware", "audit.ts"), "export default () => {};\n");
writeFileSync(join(root, "app", "queues", "mail.ts"), "export default defineQueue({});\n");
expect(checkReachability(root)).toEqual([]);
});
+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.17",
"version": "0.8.22",
"type": "module",
"main": "src/index.ts",
"exports": {
+59 -14
View File
@@ -283,6 +283,32 @@ function componentEventAttribute(name: string): string {
return `data-wrn-out-${name}`;
}
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
function expandBindings(attrs: Attr[]): Attr[] {
const output: Attr[] = [];
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: string, reactive: PageReactive): string | null {
let found = false;
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
@@ -305,6 +331,7 @@ function renderAttrs(
reactive: PageReactive | null = null,
dynamicExpressions?: string[],
): string {
attrs = expandBindings(attrs);
let bindIndex = 0;
const rendered = attrs
.map((attr) => {
@@ -463,7 +490,7 @@ function renderLoopBody(node: ViewNode, locals: string[] = []): string {
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
@@ -805,9 +832,9 @@ function renderPageComponentInvocation(
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
@@ -827,7 +854,7 @@ function renderNestedComponentInvocation(
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);
@@ -1830,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(", ")} };`
: ""
}
@@ -2251,7 +2280,7 @@ function renderClientControlTemplate(nodes: ViewNode[]): string {
const componentTag = isComponentTag(node.tag);
let bindIndex = 0;
const attrs = node.attrs
const attrs = expandBindings(node.attrs)
.map((attribute) => {
const name = attribute.event
? componentTag
@@ -2293,24 +2322,27 @@ function encodeClientControl(value: unknown): string {
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
let expression = "``";
const clientBranches: string[] = [];
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>`;
}
@@ -2489,7 +2521,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
(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);
@@ -3036,6 +3068,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";
@@ -3137,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)}"`;
}
@@ -3155,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;"));
@@ -211,6 +211,10 @@ function __wrnRaw(v: unknown): string {
return String(v == null ? "" : v);
}
function __wrnexusEncodeControl(value: unknown): string {
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}
function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) {
return "undefined";
@@ -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(
+29
View File
@@ -1070,6 +1070,25 @@ component Banner {
expect(output).toContain("visible");
expect(output).toContain("Visible");
expect(output).toContain("Hidden");
expect(output).toContain("__wrnexusEncodeControl");
});
test("bind directives compile to reactive values and assignment handlers", () => {
const output = generate(
parse(`page FormPage {
state name = ""
state active = false
view {
<Input bind:value={name} />
<input bind:value={name} />
<Switch bind:checked={active} />
}
}`),
);
expect(output).not.toContain("bind:value");
expect(output).not.toContain("bind:checked");
expect(output).toContain("name = payload.value");
expect(output).toContain("active = payload.checked");
});
test("if and each blocks emit browser control metadata while preserving SSR", () => {
@@ -1132,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.28",
"version": "0.8.35",
"type": "module",
"main": "src/index.ts",
"exports": {
+41
View File
@@ -0,0 +1,41 @@
export interface ApiRequest<Input = unknown> {
method?: string;
params?: Record<string, string | number>;
query?: Record<string, string | number | boolean | undefined>;
body?: Input;
}
export interface ApiClientOptions {
baseUrl?: string;
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
}
/** Runtime used by generated API clients; throws a typed response error on non-2xx results. */
export function createApiClient(options: ApiClientOptions = {}) {
const fetcher = options.fetch ?? globalThis.fetch;
return async function call<Output, Input = unknown>(
template: string,
request: ApiRequest<Input> = {},
): Promise<Output> {
let path = template;
for (const [name, value] of Object.entries(request.params ?? {})) {
path = path.replace(`[${name}]`, encodeURIComponent(String(value)));
}
const url = new URL(path, options.baseUrl ?? globalThis.location?.origin ?? "http://localhost");
for (const [name, value] of Object.entries(request.query ?? {}))
if (value !== undefined) url.searchParams.set(name, String(value));
const response = await fetcher(url, {
method: request.method ?? (request.body === undefined ? "GET" : "POST"),
credentials: "same-origin",
headers: request.body === undefined ? undefined : { "content-type": "application/json" },
body: request.body === undefined ? undefined : JSON.stringify(request.body),
});
const data = response.status === 204 ? undefined : await response.json();
if (!response.ok)
throw Object.assign(
new Error((data as { error?: string })?.error ?? `HTTP ${response.status}`),
{ status: response.status, data },
);
return data as Output;
};
}
+18 -1
View File
@@ -16,6 +16,23 @@ export { REACTIVE_RUNTIME } from "./reactive-runtime.ts";
export { NAV_RUNTIME } from "./nav-runtime.ts";
export { REALTIME_RUNTIME } from "./realtime-runtime.ts";
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;
@@ -73,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;
+87 -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;
@@ -1173,6 +1220,7 @@ export const REACTIVE_RUNTIME = String.raw`
* lacks one of these does not break the rest.
*/
if (name === "toast") return toastApi;
if (name === "useFetch") return useFetch;
if (boundWindowGlobals[name] && typeof window[name] === "function") {
return window[name].bind(window);
}
@@ -1817,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();
@@ -2001,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);
}
@@ -2009,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;
@@ -2398,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);
}
});
});
@@ -2774,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);
@@ -2795,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);
});
});
@@ -4350,6 +4404,29 @@ export const REACTIVE_RUNTIME = String.raw`
});
}
/**
* Public client transport for dynamic API paths.
*
* The apis block remains the typed, declarative choice for fixed routes. This
* helper covers paths assembled at runtime without exposing a private
* framework global to application code.
*/
function useFetch(path, methodOrOptions, input) {
var method = "GET";
var values = input;
if (typeof methodOrOptions === "string") {
method = methodOrOptions;
} else if (methodOrOptions && typeof methodOrOptions === "object") {
method = methodOrOptions.method || "GET";
values = method === "GET" || method === "HEAD"
? (methodOrOptions.query || methodOrOptions.params)
: (methodOrOptions.body === undefined ? methodOrOptions.data : methodOrOptions.body);
}
return wrnexusCallApi(path, method, values);
}
window.useFetch = useFetch;
// Compatibility for applications compiled before the public helper landed.
window.__wrnexusCallApi = wrnexusCallApi;
function dispatchComponentEvent(root, name, detail) {
+15
View File
@@ -64,6 +64,21 @@ test("POST sends a JSON body", async () => {
);
});
test("useFetch is the public dynamic-path transport", async () => {
const { calls, win } = harness({ status: 200, payload: { ok: true } });
const useFetch = (
win as unknown as {
useFetch: (path: string, options: { method: string; body: unknown }) => Promise<unknown>;
}
).useFetch;
await expect(
useFetch("/api/users/42/test", { method: "POST", body: { dryRun: true } }),
).resolves.toEqual({ ok: true });
expect(calls[0]!.url).toBe("/api/users/42/test");
expect(calls[0]!.init.body).toBe(JSON.stringify({ dryRun: true }));
});
test("a non-GET request carries the CSRF token from the cookie", async () => {
const { callApi, calls, win } = harness({ status: 200, payload: {} });
win.document.cookie = "wrn-csrf=token-123";
+23
View File
@@ -0,0 +1,23 @@
import { expect, test } from "bun:test";
import { createApiClient } from "../src/index.ts";
test("typed API client expands params, query and JSON input", async () => {
let request: Request | undefined;
const call = createApiClient({
baseUrl: "https://app.test",
fetch: async (input, init) => {
request = new Request(input, init);
return Response.json({ ok: true });
},
});
expect(
await call<{ ok: boolean }, { name: string }>("/api/users/[id]", {
method: "PUT",
params: { id: 7 },
query: { audit: true },
body: { name: "Ada" },
}),
).toEqual({ ok: true });
expect(request?.url).toBe("https://app.test/api/users/7?audit=true");
expect(await request?.json()).toEqual({ name: "Ada" });
});
+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-");
+13
View File
@@ -297,3 +297,16 @@ const users = await usersRepo.all({
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.
`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.
## Reusable seed helpers
```ts
import { addSeedData, removeSeedData, runSeedQuery, getDb } from "@wrnexus/db";
await addSeedData(getDb, [{ code: "free", credits: 100 }], "plans", { conflict: "ignore" });
await removeSeedData(getDb(), { code: "legacy" }, "plans");
await runSeedQuery(getDb(), "UPDATE plans SET credits = ? WHERE code = ?", [200, "free"]);
```
Table and column identifiers are validated, values are always parameterized, and empty bulk
deletes are refused unless `{ all: true }` is explicit.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/db",
"version": "0.8.18",
"version": "0.8.20",
"private": true,
"type": "module",
"main": "./src/index.ts",
+19
View File
@@ -61,3 +61,22 @@ export {
createRepository,
} from "./helpers.ts";
export type { Repository } from "./helpers.ts";
export {
addSeedData,
defineSeed,
removeSeedData,
runSeedQuery,
seedIfMissing,
seedUsers,
upsertSeedData,
} from "./seed.ts";
export type {
AddSeedOptions,
SeedDatabase,
SeedRow,
SeedStep,
SeedUserAccount,
UpsertSeedOptions,
} from "./seed.ts";
export { defineLedger } from "./ledger.ts";
export type { LedgerOptions } from "./ledger.ts";
+76
View File
@@ -0,0 +1,76 @@
import type { Db } from "./driver.ts";
const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/;
const ident = (value: string) => {
if (!SAFE.test(value)) throw new TypeError(`Unsafe ledger identifier: ${value}`);
return value;
};
export interface LedgerOptions {
db: () => Db;
table: string;
subjectColumn?: string;
amountColumn?: string;
kindColumn?: string;
reasonColumn?: string;
referenceColumn?: string;
}
export function defineLedger(options: LedgerOptions) {
const table = ident(options.table);
const subject = ident(options.subjectColumn ?? "subject_id");
const amount = ident(options.amountColumn ?? "amount");
const kind = ident(options.kindColumn ?? "kind");
const reason = ident(options.reasonColumn ?? "reason");
const reference = ident(options.referenceColumn ?? "ref");
const positive = (value: number) => {
if (!Number.isSafeInteger(value) || value < 1)
throw new RangeError("ledger amount must be a positive safe integer");
};
const write = async (
subjectId: string,
delta: number,
entryKind: string,
entryReason: string,
ref = "",
) => {
if (!subjectId.trim()) throw new TypeError("ledger subject cannot be empty");
await options
.db()
.exec(
`INSERT INTO ${table} (${subject},${amount},${kind},${reason},${reference}) VALUES (?,?,?,?,?)`,
[subjectId, delta, entryKind, entryReason, ref],
);
};
const balance = async (subjectId: string, db = options.db()) => {
const row = await db.one<{ balance: number | string | null }>(
`SELECT COALESCE(SUM(${amount}),0) AS balance FROM ${table} WHERE ${subject} = ?`,
[subjectId],
);
return Number(row?.balance ?? 0);
};
return {
balance,
async credit(subjectId: string, value: number, entryReason: string, ref = "") {
positive(value);
await write(subjectId, value, "credit", entryReason, ref);
},
async debit(subjectId: string, value: number, entryReason: string, ref = "") {
positive(value);
return options.db().tx(async (db) => {
if ((await balance(subjectId, db)) < value) return false;
await db.exec(
`INSERT INTO ${table} (${subject},${amount},${kind},${reason},${reference}) VALUES (?,?,?,?,?)`,
[subjectId, -value, "debit", entryReason, ref],
);
return true;
});
},
refund: (subjectId: string, value: number, entryReason: string, ref = "") => {
positive(value);
return write(subjectId, value, "refund", entryReason, ref);
},
entries: (subjectId: string) =>
options.db().all(`SELECT * FROM ${table} WHERE ${subject} = ? ORDER BY id DESC`, [subjectId]),
};
}
+183
View File
@@ -0,0 +1,183 @@
import type { Db, ExecResult } from "./driver.ts";
export type SeedRow = Record<string, unknown>;
export type SeedDatabase = Db | (() => Db);
export interface AddSeedOptions {
conflict?: "error" | "ignore" | "replace";
}
export interface UpsertSeedOptions {
key: string | readonly string[];
update?: readonly string[];
}
function identifier(value: string, label: string): string {
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
throw new TypeError(`${label} must be a safe SQL identifier`);
}
return value;
}
function database(value: SeedDatabase): Db {
return typeof value === "function" ? value() : value;
}
/** Insert one or many object rows with parameterized values. */
export async function addSeedData(
source: SeedDatabase,
data: SeedRow | readonly SeedRow[],
table: string,
options: AddSeedOptions = {},
): Promise<ExecResult[]> {
const db = database(source);
const target = identifier(table, "seed table");
const rows = Array.isArray(data) ? data : [data];
const results: ExecResult[] = [];
const clause =
options.conflict === "ignore"
? " OR IGNORE"
: options.conflict === "replace"
? " OR REPLACE"
: "";
for (const row of rows) {
const entries = Object.entries(row);
if (!entries.length) throw new TypeError("seed row cannot be empty");
const columns = entries.map(([column]) => identifier(column, "seed column"));
results.push(
await db.exec(
`INSERT${clause} INTO ${target} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`,
entries.map(([, value]) => value),
),
);
}
return results;
}
/** Delete matching seed rows. An empty match is refused unless `all` is explicit. */
export async function removeSeedData(
source: SeedDatabase,
match: SeedRow,
table: string,
options: { all?: boolean } = {},
): Promise<ExecResult> {
const db = database(source);
const target = identifier(table, "seed table");
const entries = Object.entries(match);
if (!entries.length && !options.all) {
throw new TypeError("removeSeedData requires match fields or { all: true }");
}
const where = entries.length
? ` WHERE ${entries.map(([column]) => `${identifier(column, "seed column")} = ?`).join(" AND ")}`
: "";
return db.exec(
`DELETE FROM ${target}${where}`,
entries.map(([, value]) => value),
);
}
/** Execute an application-owned parameterized seed statement. */
export function runSeedQuery(
source: SeedDatabase,
query: string,
data: readonly unknown[] = [],
): Promise<ExecResult> {
const db = database(source);
if (!query.trim()) throw new TypeError("seed query cannot be empty");
return db.exec(query, [...data]);
}
/** Portable object upsert for SQLite, PostgreSQL and MySQL. */
export async function upsertSeedData(
source: SeedDatabase,
data: SeedRow | readonly SeedRow[],
table: string,
options: UpsertSeedOptions,
): Promise<ExecResult[]> {
const db = database(source);
const target = identifier(table, "seed table");
const keys = (Array.isArray(options.key) ? options.key : [options.key]).map((key) =>
identifier(key, "seed key"),
);
if (!keys.length) throw new TypeError("upsertSeedData requires at least one key");
const results: ExecResult[] = [];
for (const row of Array.isArray(data) ? data : [data]) {
const entries = Object.entries(row);
if (!entries.length) throw new TypeError("seed row cannot be empty");
const columns = entries.map(([column]) => identifier(column, "seed column"));
for (const key of keys)
if (!columns.includes(key)) throw new TypeError(`seed row is missing key '${key}'`);
const updates = (options.update ?? columns.filter((column) => !keys.includes(column))).map(
(column) => identifier(column, "seed update column"),
);
const prefix = db.driver.dialect === "mysql" ? "INSERT INTO" : "INSERT INTO";
const suffix =
db.driver.dialect === "mysql"
? ` ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(", ")}`
: ` ON CONFLICT (${keys.join(", ")}) DO ${
updates.length
? `UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(", ")}`
: "NOTHING"
}`;
results.push(
await db.exec(
`${prefix} ${target} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})${suffix}`,
entries.map(([, value]) => value),
),
);
}
return results;
}
export async function seedIfMissing(
source: SeedDatabase,
table: string,
match: SeedRow,
data: SeedRow,
): Promise<boolean> {
const db = database(source);
const target = identifier(table, "seed table");
const where = Object.entries(match);
if (!where.length) throw new TypeError("seedIfMissing requires match fields");
const existing = await db.one(
`SELECT 1 AS found FROM ${target} WHERE ${where
.map(([column]) => `${identifier(column, "seed column")} = ?`)
.join(" AND ")} LIMIT 1`,
where.map(([, value]) => value),
);
if (existing) return false;
await addSeedData(db, data, target);
return true;
}
export type SeedStep = (db: Db) => void | Promise<void>;
export function defineSeed(...steps: readonly SeedStep[]): SeedStep {
return async (db) =>
db.tx(async (transaction) => {
for (const step of steps) await step(transaction);
});
}
export interface SeedUserAccount {
identifier: string;
password?: string;
[key: string]: unknown;
}
export async function seedUsers<T extends SeedUserAccount, U extends { id: string }>(
accounts: readonly T[],
options: {
find(identifier: string): Promise<U | null>;
create(account: T): Promise<U>;
configure?(user: U, account: T): void | Promise<void>;
},
): Promise<U[]> {
const users: U[] = [];
for (const account of accounts) {
const user = (await options.find(account.identifier)) ?? (await options.create(account));
await options.configure?.(user, account);
users.push(user);
}
return users;
}
+25
View File
@@ -0,0 +1,25 @@
import { expect, test } from "bun:test";
import { createDb, defineLedger, upsertSeedData } from "../src/index.ts";
import { sqlite } from "../src/adapters/sqlite.ts";
test("portable seed upserts and ledgers remove application boilerplate", async () => {
const db = createDb(sqlite(":memory:"));
await db.exec("CREATE TABLE plans (code TEXT PRIMARY KEY, credits INTEGER NOT NULL)");
await upsertSeedData(db, { code: "free", credits: 10 }, "plans", { key: "code" });
await upsertSeedData(db, { code: "free", credits: 20 }, "plans", { key: "code" });
expect((await db.one<{ credits: number }>("SELECT credits FROM plans"))?.credits).toBe(20);
await db.exec(
"CREATE TABLE ledger (id INTEGER PRIMARY KEY, user_id TEXT, delta INTEGER, kind TEXT, reason TEXT, ref TEXT)",
);
const ledger = defineLedger({
db: () => db,
table: "ledger",
subjectColumn: "user_id",
amountColumn: "delta",
});
await ledger.credit("u1", 10, "grant");
expect(await ledger.debit("u1", 4, "use")).toBe(true);
expect(await ledger.balance("u1")).toBe(6);
expect(await ledger.debit("u1", 7, "too much")).toBe(false);
await db.close();
});
+45
View File
@@ -0,0 +1,45 @@
import { expect, test } from "bun:test";
import type { Db } from "../src/driver.ts";
import { addSeedData, removeSeedData, runSeedQuery } from "../src/seed.ts";
function fakeDb() {
const calls: Array<{ sql: string; parameters: unknown[] }> = [];
const db = {
exec: async (sql: string, parameters: unknown[] = []) => {
calls.push({ sql, parameters });
return { changes: 1 };
},
} as unknown as Db;
return { db, calls };
}
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 removeSeedData(db, { code: "free" }, "plans");
expect(calls[0]).toEqual({
sql: "INSERT OR IGNORE INTO plans (code, credits) VALUES (?, ?)",
parameters: ["free", 10],
});
expect(calls[2]).toEqual({ sql: "DELETE FROM plans WHERE code = ?", parameters: ["free"] });
});
test("custom seed queries stay parameterized and unsafe identifiers are refused", async () => {
const { db, calls } = fakeDb();
await runSeedQuery(db, "UPDATE plans SET credits = ? WHERE code = ?", [50, "free"]);
expect(calls[0]?.parameters).toEqual([50, "free"]);
await expect(addSeedData(db, { id: 1 }, "plans; DROP TABLE plans")).rejects.toThrow(
"safe SQL identifier",
);
await expect(removeSeedData(db, {}, "plans")).rejects.toThrow("match fields");
});
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.8.45",
"version": "0.8.56",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -24,6 +24,8 @@
"@wrnexus/pubsub": "workspace:*",
"@wrnexus/uploader": "workspace:*",
"@wrnexus/plugin": "workspace:*",
"@wrnexus/queue": "workspace:*",
"@wrnexus/react": "workspace:*",
"@wrnexus/store": "workspace:*",
"@wrnexus/security": "workspace:*",
"@wrnexus/observability": "workspace:*",
+11
View File
@@ -33,6 +33,7 @@ import {
getDbPerformanceSnapshot,
} from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { configureQueueStorage, type QueueStorageConfig } from "@wrnexus/queue";
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
import { setAuthzCatalog, type AuthzCatalog, type AuthzModule } from "@wrnexus/authz";
import { loadAppAuthzCatalog } from "./authz-boot.ts";
@@ -102,6 +103,8 @@ export interface ServeOptions {
/** Inject the live-reload client (defaults to true in development). */
hmr?: boolean;
appConfig?: Record<string, unknown>;
/** Queue persistence; durable SQLite is used when omitted. */
queue?: QueueStorageConfig;
/** Resolved absolute path to the global CSS entry, or null. */
styleEntry?: string | null;
/** Custom styles config (e.g. a Tailwind/PostCSS processor). */
@@ -220,6 +223,7 @@ function resolveDevToolbarConfig(
export async function startServer(opts: ServeOptions): Promise<RunningServer> {
const appDir = resolve(opts.appDir);
const appRoot = dirname(appDir);
configureQueueStorage({ ...opts.queue, appRoot });
const mode: Mode = opts.mode ?? "development";
const importConfig = (opts.appConfig?.imports ?? {}) as {
mode?: "legacy" | "compatible" | "explicit";
@@ -363,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);
@@ -813,6 +823,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
watcher?.close();
unsubscribeDevToolbar?.();
server.stop();
void handlers.shutdown();
void pluginRunner.hook("shutdown");
rmSync(cacheDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
},
+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;
+13 -1
View File
@@ -40,6 +40,7 @@ import { VALIDATE_RUNTIME } from "@wrnexus/validation";
import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n";
import { setDb, registerLazyDb, getDb, hasDb, migrate } from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { configureQueueStorage, type QueueStorageConfig } from "@wrnexus/queue";
import { hasAuthzCatalog, mergeCatalogs, setAuthzCatalog, type AuthzModule } from "@wrnexus/authz";
import {
configureStorage,
@@ -79,6 +80,8 @@ export interface ProdManifest {
layouts: { name: string; mod: RouteModule }[];
/** RPC service implementations (from app/services/*.ts). */
services?: { name: string; mod: RouteModule }[];
/** Background queue modules (from app/queues/*.ts). */
queues?: { name: string; mod: RouteModule }[];
}
export interface ProductionPluginAsset {
@@ -136,6 +139,8 @@ export interface ProdOptions {
db?: { driver: string; url: string };
/** Named databases, reached with `getDb("<name>")`. */
databases?: Record<string, { driver: string; url: string }>;
/** Queue persistence; durable SQLite is used when omitted. */
queue?: QueueStorageConfig;
/**
* Absolute path to the default db's migrations bundled into the build
* (`dist/migrations`). When set, they are applied on startup like dev.
@@ -292,6 +297,7 @@ function buildProdRouter(manifest: ProdManifest): {
for (const l of manifest.layouts) modules.set(`layout:${l.name}`, l.mod);
for (const service of manifest.services ?? [])
modules.set(`service:${service.name}`, service.mod);
for (const queue of manifest.queues ?? []) modules.set(`queue:${queue.name}`, queue.mod);
const router: Router = {
pages,
@@ -307,6 +313,10 @@ function buildProdRouter(manifest: ProdManifest): {
name: service.name,
file: `service:${service.name}`,
})),
queues: (manifest.queues ?? []).map((queue) => ({
name: queue.name,
file: `queue:${queue.name}`,
})),
matchPage: optimizedMatcher(pages),
matchApi: optimizedMatcher(api),
matchRealtime: optimizedMatcher(realtime),
@@ -492,6 +502,7 @@ export function createProductionHandlers(
for (const [name, cfg] of Object.entries(opts.databases ?? {})) {
registerLazyDb(name, () => connectFromConfig(cfg));
}
configureQueueStorage({ ...opts.queue, appRoot: process.cwd() });
// File-upload storage. Relative local dirs resolve against the deployment cwd
// (NOT dist/, which is rebuilt) so uploads persist across deploys.
@@ -652,11 +663,12 @@ export async function createProductionServer(manifest: ProdManifest, opts: ProdO
// Graceful shutdown: stop accepting connections, then exit.
let shuttingDown = false;
const shutdown = () => {
const shutdown = async () => {
if (shuttingDown) return;
shuttingDown = true;
console.log("WrNexus: shutting down…");
server.stop();
await handlers.shutdown();
process.exit(0);
};
process.on("SIGTERM", shutdown);
+86 -2
View File
@@ -1,4 +1,5 @@
import { brotliCompressSync, constants as zlibConstants } from "node:zlib";
import { Buffer } from "node:buffer";
/**
* Shared request runtime used by BOTH the dev server and the production server.
*
@@ -909,6 +910,7 @@ export interface Ws {
export interface Handlers {
fetch(req: Request, server: UpgradeServer): Promise<Response | undefined>;
shutdown(): Promise<void>;
websocket: {
open(ws: Ws): void;
message(ws: Ws, message: string | Uint8Array): void;
@@ -985,6 +987,27 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}
return servicesPromise;
};
type QueueModule = { start(): void; shutdown(options?: { force?: boolean }): Promise<void> };
let queuesPromise: Promise<QueueModule[]> | undefined;
const loadQueues = (): Promise<QueueModule[]> => {
queuesPromise ??= (async () => {
const queues: QueueModule[] = [];
for (const entry of router.queues ?? []) {
const imported = await loadModule(entry.file);
const queue = imported.default as QueueModule | undefined;
if (!queue || typeof queue.start !== "function" || typeof queue.shutdown !== "function") {
throw new Error(`Queue ${entry.file} must default-export defineQueue(...)`);
}
queue.start();
queues.push(queue);
}
return queues;
})().catch((error) => {
queuesPromise = undefined;
throw error;
});
return queuesPromise;
};
// Server-side realtime room manager (shared by every `defineRoom` connection).
const realtime = createRealtimeRegistry();
@@ -1041,6 +1064,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
};
async function fetchHandler(req: Request, server: UpgradeServer): Promise<Response | undefined> {
await loadQueues();
// Behind a trusted proxy (nginx / gateway), honor X-Forwarded-Proto/Host so
// ctx.url reflects the external HTTPS scheme — makes CSRF/session cookies Secure.
const url = resolveRequestUrl(req, deps.security?.trustProxy);
@@ -1644,6 +1668,34 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
language?: string,
depth = 0,
): Promise<string> {
// Control definitions are inert base64 JSON. Resolve component mounts in
// their branch templates too, otherwise a component first selected by
// client state would materialize only an empty data-component placeholder.
const controlPattern = /data-wrn-(?:if|each)="([A-Za-z0-9+/=]+)"/g;
const replacements: Array<{ from: string; to: string }> = [];
for (const match of body.matchAll(controlPattern)) {
try {
const definition = JSON.parse(Buffer.from(match[1]!, "base64").toString("utf8")) as
Array<{ body?: string }> | { body?: string; empty?: string };
const entries = Array.isArray(definition) ? definition : [definition];
for (const entry of entries) {
if (typeof entry.body === "string" && entry.body.includes("data-component="))
entry.body = await renderComponents(entry.body, translate, language, depth + 1);
if (
"empty" in entry &&
typeof entry.empty === "string" &&
entry.empty.includes("data-component=")
)
entry.empty = await renderComponents(entry.empty, translate, language, depth + 1);
}
const encoded = Buffer.from(JSON.stringify(definition), "utf8").toString("base64");
replacements.push({ from: match[0], to: match[0].replace(match[1]!, encoded) });
} catch {
// The client reports malformed control metadata with source context.
}
}
for (const replacement of replacements) body = body.replace(replacement.from, replacement.to);
if (depth > 15 || router.components.length === 0 || !body.includes("data-component=")) {
return body;
}
@@ -1687,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) => ({
@@ -1717,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(
@@ -1898,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);
@@ -2321,6 +2396,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
return {
fetch: fetchHandler,
async shutdown() {
const queues = await queuesPromise?.catch(() => []);
await Promise.allSettled((queues ?? []).map((queue) => queue.shutdown()));
},
websocket: {
open(ws) {
if (ws.data?.kind === "hmr") hub?.add(ws);
@@ -2612,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;
+1
View File
@@ -58,6 +58,7 @@ const server = await startServer({
i18n: config.i18n,
db: config.db,
databases: config.databases,
queue: config.queue,
realtime: config.realtime,
storage: config.storage,
mobile: config.mobile,
@@ -48,6 +48,134 @@ test("a component nested in another component's slot keeps its boundary", async
expect(html).not.toContain('data-component="Badge"');
});
test("a component in an initially hidden client branch is server-prepared for instantiation", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-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/Input.wrn"), "component Input { view { <input /> } }\n");
const definition = Buffer.from(
JSON.stringify([{ cond: "open", body: '<div data-component="Input" value="ready"></div>' }]),
"utf8",
).toString("base64");
const handlers = createHandlers({
mode: "development",
hmr: false,
router: buildRouter(app),
loadModule: async (file) =>
basename(file) === "Input.wrn"
? {
render: (props: Record<string, string>) =>
`<label><input value="${props.value}"></label>`,
}
: {
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();
const encoded = /data-wrn-if="([A-Za-z0-9+/=]+)"/.exec(html)?.[1];
expect(encoded).toBeTruthy();
const branches = JSON.parse(Buffer.from(encoded!, "base64").toString("utf8"));
expect(branches[0].body).toContain('<input value="ready">');
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");
});
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/encryption",
"version": "0.8.9",
"version": "0.8.10",
"private": true,
"type": "module",
"main": "./src/index.ts",
@@ -23,6 +23,7 @@
"typescript": "^6.0.3"
},
"dependencies": {
"@wrnexus/core": "workspace:*"
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*"
}
}
+2
View File
@@ -160,3 +160,5 @@ export type {
DecryptedHttpBody,
ReplayStore,
} from "./http.ts";
export { defineSecretResource } from "./resource.ts";
export type { SecretResourceOptions } from "./resource.ts";
+53
View File
@@ -0,0 +1,53 @@
import type { Db } from "@wrnexus/db";
import { decrypt, encrypt } from "./index.ts";
export interface SecretResourceOptions<T extends Record<string, unknown>> {
db: () => Db;
table: string;
key: string | (() => string);
encrypted: readonly (keyof T & string)[];
hidden?: readonly (keyof T & string)[];
id?: string;
}
const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/;
/** Encrypt selected database fields and guarantee hidden fields never leave `safe()`. */
export function defineSecretResource<T extends Record<string, unknown>>(
options: SecretResourceOptions<T>,
) {
const checked = (value: string) => {
if (!SAFE.test(value)) throw new TypeError(`Unsafe secret resource identifier: ${value}`);
return value;
};
const table = checked(options.table);
const id = checked(options.id ?? "id");
const encrypted = options.encrypted.map(checked);
const hidden = new Set((options.hidden ?? options.encrypted).map(checked));
const key = () => (typeof options.key === "function" ? options.key() : options.key);
return {
async seal(input: T): Promise<T> {
const output = { ...input };
const writable = output as Record<string, unknown>;
for (const field of encrypted)
if (writable[field] !== undefined)
writable[field] = await encrypt(String(writable[field]), key());
return output;
},
async open(input: T): Promise<T> {
const output = { ...input };
const writable = output as Record<string, unknown>;
for (const field of encrypted)
if (writable[field]) writable[field] = await decrypt(String(writable[field]), key());
return output;
},
safe(input: T): Partial<T> {
return Object.fromEntries(
Object.entries(input).filter(([field]) => !hidden.has(field)),
) as Partial<T>;
},
async find(value: string | number): Promise<T | null> {
return options.db().one<T>(`SELECT * FROM ${table} WHERE ${id} = ?`, [value]);
},
};
}
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/helpers",
"version": "0.8.8",
"version": "0.8.9",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
@@ -9,6 +9,8 @@
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/core": "workspace:*"
}
}
+90
View File
@@ -0,0 +1,90 @@
import type { Context } from "@wrnexus/core";
import { can } from "@wrnexus/authz";
export class HttpError extends Error {
constructor(
public readonly status: number,
message: string,
public readonly details?: unknown,
) {
super(message);
this.name = "HttpError";
}
}
export function subjectId(ctx: Pick<Context, "user">): string {
return String((ctx.user as { id?: unknown } | undefined)?.id ?? "").trim();
}
export function requireUser(ctx: Pick<Context, "user">): { id: string; user: unknown } {
const id = subjectId(ctx);
if (!id) throw new HttpError(401, "Unauthorized");
return { id, user: ctx.user };
}
export function requireParam(
ctx: Pick<Context, "params">,
name: string,
options: { integer?: boolean; positive?: boolean } = {},
): string | number {
const raw = String(ctx.params[name] ?? "").trim();
if (!raw) throw new HttpError(400, `Route parameter '${name}' is required.`);
if (!options.integer) return raw;
const value = Number(raw);
if (!Number.isInteger(value) || (options.positive && value < 1)) {
throw new HttpError(400, `Route parameter '${name}' must be a positive integer.`);
}
return value;
}
export async function requireJson<T = Record<string, unknown>>(
ctx: Pick<Context, "req">,
): Promise<T> {
try {
return (await ctx.req.json()) as T;
} catch {
throw new HttpError(400, "Request body must be valid JSON.");
}
}
export async function requirePermission(
ctx: Context,
permission: string,
resource?: unknown,
): Promise<void> {
if (!(await can(ctx, permission, resource))) throw new HttpError(403, "Forbidden");
}
export const json = {
ok: <T>(value: T) => Response.json(value),
created: <T>(value: T) => Response.json(value, { status: 201 }),
noContent: () => new Response(null, { status: 204 }),
error: (status: number, message: string, details?: unknown) =>
Response.json({ error: message, ...(details === undefined ? {} : { details }) }, { status }),
};
export type ApiHandler = (ctx: Context) => Response | Promise<Response>;
/** Convert thrown HttpError values into the framework's standard JSON error shape. */
export function defineApiRoute(handler: ApiHandler): ApiHandler {
return async (ctx) => {
try {
return await handler(ctx);
} catch (error) {
if (error instanceof HttpError) return json.error(error.status, error.message, error.details);
throw error;
}
};
}
export function authorized(
permission: string,
handler: ApiHandler,
resource?: (ctx: Context) => unknown | Promise<unknown>,
): ApiHandler {
return defineApiRoute(async (ctx) => {
requireUser(ctx);
await requirePermission(ctx, permission, await resource?.(ctx));
return handler(ctx);
});
}
+14
View File
@@ -157,3 +157,17 @@ export {
once,
} from "./resilience.ts";
export type { RetryOptions } from "./resilience.ts";
export {
HttpError,
authorized,
defineApiRoute,
json,
requireJson,
requireParam,
requirePermission,
requireUser,
subjectId,
} from "./http.ts";
export type { ApiHandler } from "./http.ts";
export { defineResource } from "./resource.ts";
export type { ResourceDefinition, ResourceHandlers } from "./resource.ts";
+118
View File
@@ -0,0 +1,118 @@
import type { Context } from "@wrnexus/core";
import type { Db } from "@wrnexus/db";
import { can } from "@wrnexus/authz";
import { HttpError, defineApiRoute, json, requireJson, requireParam, requireUser } from "./http.ts";
const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/;
const ident = (value: string) => {
if (!SAFE.test(value)) throw new TypeError(`Unsafe resource identifier: ${value}`);
return value;
};
export interface ResourceDefinition<T extends Record<string, unknown>> {
name: string;
db: () => Db;
table: string;
id?: string;
owner?: string;
fields: readonly (keyof T & string)[];
permissions?: { read?: string; readAny?: string; write?: string; writeAny?: string };
validate?: (input: unknown, mode: "create" | "update") => T | Promise<T>;
serialize?: (row: T) => unknown;
}
export interface ResourceHandlers {
list(ctx: Context): Response | Promise<Response>;
create(ctx: Context): Response | Promise<Response>;
get(ctx: Context): Response | Promise<Response>;
update(ctx: Context): Response | Promise<Response>;
remove(ctx: Context): Response | Promise<Response>;
}
/** Build conventional owned CRUD handlers from one checked resource declaration. */
export function defineResource<T extends Record<string, unknown>>(
definition: ResourceDefinition<T>,
): ResourceHandlers {
const table = ident(definition.table);
const id = ident(definition.id ?? "id");
const owner = definition.owner ? ident(definition.owner) : undefined;
const fields = definition.fields.map(ident);
const shape = (row: T) => definition.serialize?.(row) ?? row;
const authorize = async (ctx: Context, action: "read" | "write", row?: T) => {
const subject = requireUser(ctx).id;
const permission = definition.permissions?.[action];
const any = definition.permissions?.[`${action}Any`];
if (any && (await can(ctx, any))) return subject;
if (
permission &&
!(await can(ctx, permission, owner ? { ownerId: row?.[owner] ?? subject } : row))
) {
throw new HttpError(403, "Forbidden");
}
if (owner && row && String(row[owner]) !== subject) throw new HttpError(403, "Forbidden");
return subject;
};
const find = async (ctx: Context) => {
const value = requireParam(ctx, id, { integer: true, positive: true });
const row = await definition.db().one<T>(`SELECT * FROM ${table} WHERE ${id} = ?`, [value]);
if (!row) throw new HttpError(404, `${definition.name} not found`);
return row;
};
return {
list: defineApiRoute(async (ctx) => {
const subject = await authorize(ctx, "read");
const seeAll =
definition.permissions?.readAny && (await can(ctx, definition.permissions.readAny));
const rows = await definition
.db()
.all<T>(
`SELECT * FROM ${table}${owner && !seeAll ? ` WHERE ${owner} = ?` : ""} ORDER BY ${id} DESC`,
owner && !seeAll ? [subject] : [],
);
return json.ok({ [definition.name]: rows.map(shape) });
}),
create: defineApiRoute(async (ctx) => {
const subject = await authorize(ctx, "write");
const input = definition.validate
? await definition.validate(await requireJson(ctx), "create")
: await requireJson<T>(ctx);
const values = Object.fromEntries(
fields.filter((key) => key in input).map((key) => [key, input[key]]),
);
if (owner) values[owner] = subject;
const columns = Object.keys(values).map(ident);
const result = await definition.db().exec(
`INSERT INTO ${table} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`,
columns.map((key) => values[key]),
);
return json.created({ ok: true, id: result.lastInsertId });
}),
get: defineApiRoute(async (ctx) => {
const row = await find(ctx);
await authorize(ctx, "read", row);
return json.ok({ [definition.name.replace(/s$/, "")]: shape(row) });
}),
update: defineApiRoute(async (ctx) => {
const row = await find(ctx);
await authorize(ctx, "write", row);
const input = definition.validate
? await definition.validate(await requireJson(ctx), "update")
: await requireJson<T>(ctx);
const keys = fields.filter((key) => key in input);
if (!keys.length) throw new HttpError(400, "No writable fields supplied.");
await definition
.db()
.exec(`UPDATE ${table} SET ${keys.map((key) => `${key} = ?`).join(", ")} WHERE ${id} = ?`, [
...keys.map((key) => input[key]),
row[id],
]);
return json.ok({ ok: true });
}),
remove: defineApiRoute(async (ctx) => {
const row = await find(ctx);
await authorize(ctx, "write", row);
await definition.db().exec(`DELETE FROM ${table} WHERE ${id} = ?`, [row[id]]);
return json.ok({ ok: true });
}),
};
}
+5
View File
@@ -0,0 +1,5 @@
# @wrnexus/mail
Driver-neutral transactional mail with templates, sandbox allowlists, connection tests and
optional durable queue producers. Applications supply an SMTP/API driver, while policy and
message rendering stay portable.
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@wrnexus/mail",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/encryption": "workspace:*",
"@wrnexus/queue": "workspace:*"
}
}
+197
View File
@@ -0,0 +1,197 @@
import type { DefinedJob } from "@wrnexus/queue";
import { createKeyring, open, seal, type EncryptionKeyring } from "@wrnexus/encryption";
export interface MailMessage {
from?: string;
to: string | readonly string[];
subject: string;
html?: string;
text?: string;
headers?: Record<string, string>;
}
export interface MailDriver {
send(message: MailMessage): Promise<unknown>;
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;
text?(data: T): string;
}
export interface MailOptions {
driver: MailDriver;
from?: string;
sandbox?: { enabled?: boolean; allowlist?: readonly string[] };
}
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(
(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[]) => {
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) {
const recipients = typeof message.to === "string" ? [message.to] : [...message.to];
assertAllowed(recipients);
return options.driver.send({ ...message, from: message.from ?? options.from });
},
async render<T>(template: MailTemplate<T>, data: T, to: string | readonly string[]) {
return {
from: options.from,
to,
subject: template.subject(data),
html: template.html?.(data),
text: template.text?.(data),
} satisfies MailMessage;
},
test: () => options.driver.test?.() ?? Promise.resolve({ ok: true }),
};
}
export function defineMailTemplate<T>(template: MailTemplate<T>): MailTemplate<T> {
return template;
}
export function interpolateTemplate(source: string, data: Record<string, unknown>): string {
return source.replace(/\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}/g, (_match, path: string) => {
let value: unknown = data;
for (const part of path.split("."))
value =
value && typeof value === "object" ? (value as Record<string, unknown>)[part] : undefined;
return value == null ? "" : String(value);
});
}
export function storedMailTemplate(template: {
subject: string;
html?: string;
text?: string;
}): MailTemplate<Record<string, unknown>> {
return {
subject: (data) => interpolateTemplate(template.subject, data),
html:
template.html === undefined ? undefined : (data) => interpolateTemplate(template.html!, data),
text:
template.text === undefined ? undefined : (data) => interpolateTemplate(template.text!, data),
};
}
/** Queue a fully rendered message without coupling the mail package to one queue backend. */
export function queuedMail<T extends MailMessage>(job: Pick<DefinedJob<T>, "add">) {
return { send: (message: T) => job.add(message) };
}
+73
View File
@@ -0,0 +1,73 @@
import { expect, test } from "bun:test";
import {
defineMail,
defineMailTemplate,
defineSealedMailCredentials,
mailSandboxDecision,
sealedMailDriver,
} from "../src/index.ts";
test("mail templates render and sandbox blocks accidental recipients", async () => {
const sent: unknown[] = [];
const mail = defineMail({
from: "hello@example.com",
sandbox: { enabled: true, allowlist: ["dev@example.com"] },
driver: { send: async (message) => void sent.push(message) },
});
const welcome = defineMailTemplate<{ name: string }>({
subject: ({ name }) => `Hello ${name}`,
html: ({ name }) => `<b>${name}</b>`,
});
await mail.send(await mail.render(welcome, { name: "Ada" }, "dev@example.com"));
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.
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@wrnexus/metering",
"version": "0.8.3",
"private": true,
"type": "module",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
+156
View File
@@ -0,0 +1,156 @@
export interface EntitlementPlan {
code: string;
name: string;
features: readonly string[];
allowance?: number;
price?: number;
[key: string]: unknown;
}
export interface EntitlementsOptions<TPlan extends EntitlementPlan> {
plans: () => Promise<readonly TPlan[]> | readonly TPlan[];
subscriptionFor: (subjectId: string) => Promise<string | null | undefined>;
fallback: string;
}
/** Resolve plans, features, and allowances from one server-owned catalog. */
export function defineEntitlements<TPlan extends EntitlementPlan>(
options: EntitlementsOptions<TPlan>,
) {
const all = async () => [...(await options.plans())];
const planFor = async (subjectId: string): Promise<TPlan> => {
const plans = await all();
const requested = await options.subscriptionFor(subjectId);
const plan =
plans.find((entry) => entry.code === requested) ??
plans.find((entry) => entry.code === options.fallback);
if (!plan) throw new Error(`WRN-ENTITLEMENTS: fallback plan '${options.fallback}' is missing`);
return plan;
};
return {
plans: all,
planFor,
async enabled(subjectId: string, feature: string) {
return (await planFor(subjectId)).features.includes(feature);
},
async allowance(subjectId: string) {
return Number((await planFor(subjectId)).allowance ?? 0);
},
};
}
export interface MeterPack {
code: string;
units: number;
[key: string]: unknown;
}
/** Prototype-safe, immutable pack lookup; clients select a code, never an amount. */
export function definePacks<TPack extends MeterPack>(entries: readonly TPack[]) {
const packs = new Map<string, Readonly<TPack>>();
for (const entry of entries) {
if (!entry.code.trim()) throw new TypeError("meter pack code cannot be empty");
if (!Number.isSafeInteger(entry.units) || entry.units < 1)
throw new RangeError(`meter pack '${entry.code}' units must be a positive safe integer`);
if (packs.has(entry.code)) throw new TypeError(`duplicate meter pack '${entry.code}'`);
packs.set(entry.code, Object.freeze({ ...entry }));
}
return Object.freeze({
get: (code: string) => packs.get(code),
has: (code: string) => packs.has(code),
list: () => [...packs.values()],
});
}
export type MeterResult = { ok: true } | { ok: false; reason: string; fault?: true };
export type MeterKind = "grant" | "purchase" | "refund" | "adjust";
export interface MeterStore {
balance(subjectId: string): Promise<number>;
reserve(subjectId: string, units: number, reason: string, reference: string): Promise<boolean>;
write(
subjectId: string,
units: number,
kind: MeterKind,
reason: string,
reference: string,
): Promise<void>;
}
export interface MeterOptions {
store: MeterStore;
label?: string;
onFault?: (error: unknown, operation: string) => void;
}
/** Non-throwing metering facade with validated units and explicit storage faults. */
export function defineMeter(options: MeterOptions) {
const label = options.label ?? "units";
const valid = (units: number): MeterResult | null =>
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 {
ok: false,
reason: error instanceof Error ? error.message : String(error),
fault: true,
};
};
const add = async (
kind: MeterKind,
subjectId: string,
units: number,
reason: string,
reference = "",
): Promise<MeterResult> => {
const invalid = valid(units);
if (invalid) return invalid;
try {
await options.store.write(subjectId, units, kind, reason, reference);
return { ok: true };
} catch (error) {
return fault(error, kind);
}
};
return {
balance: options.store.balance,
async reserve(
subjectId: string,
units: number,
reason: string,
reference = "",
): Promise<MeterResult> {
const invalid = valid(units);
if (invalid) return invalid;
try {
return (await options.store.reserve(subjectId, units, reason, reference))
? { ok: true }
: { ok: false, reason: "insufficient balance" };
} catch (error) {
return fault(error, "reserve");
}
},
grant: (subjectId: string, units: number, reason: string, reference = "") =>
add("grant", subjectId, units, reason, reference),
purchase: (subjectId: string, units: number, reason: string, reference = "") =>
add("purchase", subjectId, units, reason, reference),
refund: (subjectId: string, units: number, reason: string, reference = "") =>
add("refund", 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");
}
},
};
}
+66
View File
@@ -0,0 +1,66 @@
import { expect, test } from "bun:test";
import { defineEntitlements, defineMeter, definePacks } from "../src/index.ts";
test("resolves fallback entitlements and features", async () => {
const entitlements = defineEntitlements({
plans: () => [
{ code: "free", name: "Free", features: [], allowance: 10 },
{ code: "pro", name: "Pro", features: ["schedule"], allowance: 100 },
],
subscriptionFor: async () => "pro",
fallback: "free",
});
expect(await entitlements.enabled("u1", "schedule")).toBe(true);
expect(await entitlements.allowance("u1")).toBe(100);
});
test("packs are immutable and selected only by code", () => {
const packs = definePacks([{ code: "small", units: 100, label: "Small" }]);
expect(packs.get("constructor")).toBeUndefined();
expect(packs.get("small")?.units).toBe(100);
});
test("meter distinguishes refusals from storage faults", async () => {
const meter = defineMeter({
store: {
balance: async () => 5,
reserve: async (_subject, units) => units <= 5,
write: async () => {
throw new Error("offline");
},
},
});
expect(await meter.reserve("u1", 6, "send")).toEqual({
ok: false,
reason: "insufficient balance",
});
expect(await meter.grant("u1", 5, "plan")).toEqual({
ok: false,
reason: "offline",
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";

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