Compare commits

..
102 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
Clintchiz a3ddd39b7b feat: centralize application framework primitives
Quality / quality (ubuntu-latest) (push) Failing after 14m38s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-22 23:07:46 +05:30
ClintchizandClaude Opus 5 96e082b943 release: patch syntax, compiler, db, csr
Six fixes, all found by driving a real application rather than by the suite:

- syntax: a quote or brace inside a regex literal unbalanced the brace scanner
- syntax: block comments between members failed to parse, while the same
  comment inside a braced body was fine
- compiler: pages never emitted `data-wrn-loop-locals`, so a loop variable in
  a handler threw ReferenceError at click time with a green build
- csr: client-rendered `data-for` items never carried the marker either, so a
  component's output binding silently dropped every call while a plain DOM
  handler in the same position worked
- db: the query generator baked the checkout's line endings into generated
  SQL literals, so every build dirtied the working tree

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:09:38 +05:30
ClintchizandClaude Opus 5 2299a0726d chore: fit the loop-locals fix within the production gate
Three things the gate caught that the test suite could not.

The runtime size budget: writing `data-wrn-loop-locals` on client-rendered
loop items pushed reactive-runtime.ts to 51,603 against a 51,400 budget that
had only 125 bytes of headroom. Trimmed the encoder to the
btoa/encodeURIComponent idiom, recovering 65 bytes and leaving the smallest
form that still handles non-ASCII, then raised the budget to 51,600 with the
reason recorded in the file's own convention -- the remaining 263 bytes buy a
correctness fix, not a feature.

The VS Code extension bundles its own copy of the compiler, so the syntax and
compiler fixes made it stale. Rebuilt.

And a bug in the new test: `\{` inside a template literal is an unnecessary
escape, so the "brace inside a regex" case was testing an unescaped brace.
`\{` tests the case it was written for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 12:08:07 +05:30
ClintchizandClaude Opus 5 2d0df4efc9 fix(csr): write loop locals onto client-rendered for-loop items
A client `data-for` passed its loop locals to hydration in memory but never
wrote the `data-wrn-loop-locals` attribute the SSR path writes. Anything that
resolves locals by READING the DOM -- notably a component's `data-wrn-out-*`
output binding, which calls `decodeLoopLocals(componentRoot)` -- therefore
found nothing and silently dropped the call, with no console error.

A plain DOM handler kept working, because it receives locals through the
hydration closure instead, which is what made the failure look arbitrary: the
same loop variable resolved for `@click` and vanished for a component output.

Both loop paths write the marker now, keyed and non-keyed, so the DOM is the
single source of truth. Encoding goes through UTF-8 before base64 as the
server's does; `btoa` on a raw string throws above U+00FF, which would take the
whole loop down for an ordinary non-ASCII label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 11:31:11 +05:30
ClintchizandClaude Opus 5 20783699ac test(compiler): prove page loop locals render, not just emit
The existing tests assert the marker is emitted. This one executes the
generated module and asserts the rendered HTML carries each item's real,
decodable values -- generated text that reads correctly can still render
wrong, and what matters is what the runtime finds in the DOM at click time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 07:38:40 +05:30
ClintchizandClaude Opus 5 d8305a5a14 fix(db): normalise line endings when parsing queries
The generator embeds each query's SQL as a string literal, taking whatever line
endings the checkout happened to have. On a CRLF checkout every regenerated
query differed from the committed one by `\n` -> `\r\n`, so `wrnexus build`
dirtied the working tree and that churn buried real changes in the same file --
which is how a hand-applied edit ends up preferable to running the generator.

Line endings carry no meaning in SQL, so normalise on parse and let generated
output be stable across platforms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 07:28:29 +05:30
ClintchizandClaude Opus 5 98a46091b5 fix(syntax): allow block comments between members
`skipTrivia` skipped `// line comments` but not `/* block comments */`, so one
written between two page or component members failed with a bare "Unexpected
character '/'". Block comments inside a braced body already worked, which made
the failure look arbitrary: the same comment parsed or did not depending on
whether it happened to sit inside a block.

`startsWithBlockComment` now skips only whitespace and line comments, so
`props {}` keeps refusing block comments with its own explained error rather
than silently swallowing one and dropping the declaration after it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:50:51 +05:30
ClintchizandClaude Opus 5 2c50a07ed2 fix(compiler): expose {#each} locals to event handlers on pages
A handler expression is emitted as text and evaluated when the event fires, so
any loop variable it names has to travel with the element. Components emitted
`data-wrn-loop-locals` for this; pages did not. The same view worked inside a
component and threw ReferenceError inside a page -- with a green build and green
tests, since nothing renders the page in a browser during a build.

The CSR runtime already resolves locals generically via
closest("[data-wrn-loop-locals]"), so only codegen needed to change.

The marker is emitted only on elements that actually bind an event, and the
encoder only when a marker was produced -- but it MUST be emitted whenever one
is, or the render throws on an undefined function instead of the handler
throwing on an undefined variable, which is strictly worse. Covered by its own
test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:05:10 +05:30
ClintchizandClaude Opus 5 9746e8e875 fix(syntax): do not let a regex literal unbalance a block
The brace scanner knew about strings and comments but had no case for regex
literals. A quote inside one opened a phantom string that swallowed every brace
until the next quote; a lone `{` or `}` inside one miscounted block depth. Both
failed the component with "Unbalanced braces" pointing at the block's first line.

`/-/g` parsed fine, which is why this went unnoticed -- it needs a quote or a
brace inside the pattern to bite.

Regex-vs-division is decided by scanning back to the last significant
character, erring towards division: mistaking division for a regex would
swallow code to the next `/` and lose any braces between. A regex cannot span a
newline, so an unterminated one on the line is treated as "not a regex", which
is what keeps a bare URL in view text intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 06:05:09 +05:30
Clintchiz 0ddb481159 release: patch csr, ui
Quality / quality (ubuntu-latest) (push) Failing after 6m3s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-21 16:32:16 +05:30
ClintchizandClaude Opus 5 3441b96362 fix(csr): deliver focus and blur outputs from inside a component
A parent writing @focus on a component tag never heard that component's
own input or button take focus. The runtime bound every output-named DOM
fallback listener in the bubble phase, and focus and blur do not bubble,
so the event fired on the descendant and stopped there. Nothing errored --
the binding simply did nothing.

That made a whole class of declared outputs undeliverable: button.focus,
button.blur, TextLink.focus, TextLink.blur, WysiwygEditor.focus and
WysiwygEditor.blur all advertised events they could never send.

The ui ratchet for outputs nothing emits excluded natively-named outputs
on the grounds that a native event reaches the root anyway. That holds for
click and change, which bubble, and was wrong for focus and blur. Binding
those two in the capture phase makes the exclusion honest rather than
convenient; the ratchet's comment now says so.

Also documents WysiwygEditor as the chrome shell it is: it emits none of
its four outputs itself, it forwards whatever the slotted control raises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:31:33 +05:30
Clintchiz 84e58b1798 release: patch compiler
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-21 09:32:46 +05:30
ClintchizandClaude Opus 5 de4477dd9c fix(compiler): never emit an empty pattern attribute
An empty pattern compiles to a regex matching only the empty string, so
every typed value becomes invalid and the form silently refuses to submit
-- no error, no request. @wrnexus/ui's input declares pattern: string = ""
and renders pattern="{pattern}", so every input that did not opt into a
pattern shipped one that could never match. This broke sign-up in a real
app, and only became visible once the dev-server client-module fix let
form enhancements mount at all.

Attributes reach the output through two emitters and both needed it: a
component's interpolated value is baked at render time, so the whole
attribute is now emitted by __wrnOptionalAttr, while a page's static
element is dropped at compile time. Component mounts are excluded, where
the value is a prop being passed down rather than an attribute.

minlength/maxlength/min/max/step/inputmode/accept get the same treatment --
inert when empty, but meaningless too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:32:32 +05:30
Clintchiz 2970d5fff3 release: patch dev-server, security
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-21 09:04:50 +05:30
ClintchizandClaude Opus 5 eeef2d79df fix(dev-server,security): repair two defects that only appear in a published build
The dev server shipped two entries, index and serve-entry, bundled
independently because the publish build set splitting:false. They share
pipeline.ts, which holds mutable module state -- compileCacheDir, set once
at startup by the bootstrap, and browserArtifactPaths, populated during
compilation and read when serving /__wrnexus/client/*. Duplicating the
module duplicated the state, so the writer and the reader addressed
different copies: every component client module 404'd and .wrn compilation
wrote nothing. It works from source, where there is one module instance,
which is why it reached a release. Emitting a shared chunk fixes it for
every package at once.

resetDevCache also ran several hundred lines after the plugin virtual
modules were written into the same directory, deleting them at every boot.
An app with no plugins never noticed; an app with one lost them every time.

Separately, secureCookieOptions spread ...options after its path default,
and setSecureCookie always forwards an explicit path key -- so omitting
path emitted a cookie with no Path at all, which the browser then scoped to
the request's directory.

Verified end to end against a real app installing the published packages:
17 artifacts written, client modules 200, and the sign-in form submits from
the UI and reaches /dashboard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 09:04:12 +05:30
ClintchizandClaude Opus 5 ef6de302e9 chore(vscode): prepare 0.8.9 marketplace release
Quality / quality (ubuntu-latest) (push) Failing after 9m50s
Quality / quality (windows-latest) (push) Canceled after 0s
Bumps the extension past the published 0.8.8 so the apis { } editor
tooling can ship: highlighting, api. completion and hover, the api=
attribute, the repositioned removed-block diagnostics, and the fix for
the spurious "Cannot find name 'api'" error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:37:11 +05:30
ClintchizandClaude Opus 5 a970d04248 release: patch auth, cli, compiler, core, csr, dev-server, language-server, store, styles, syntax, typecheck, ui
Quality / quality (ubuntu-latest) (push) Failing after 11m0s
Quality / quality (windows-latest) (push) Canceled after 0s
Ships the apis { } block, the legacy/config cleanup, the wrnexus update
migrations, and the editor tooling that understands all of it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:15:08 +05:30
ClintchizandClaude Opus 5 5429057a38 fix(language-server): gate api hover to actual api.<name> references
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
Hover fired on whatever word was under the cursor, so a local variable
colliding with a declared block name reported the block's method and path
instead of its own hover info. Completion was already gated this way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 19:03:46 +05:30
ClintchizandClaude Opus 5 e944fd4496 feat(editor): complete tooling support for the apis block
Task 6 Step 2 was already performed and confirmed the assertion error
from the apis-block type checks lands on the apis { } block itself
(WRN-TYPE-2344), not on the offending entry, so it already surfaces
usefully and needed no relocation mapping.

That observation surfaced a real pre-existing bug: the virtual
TypeScript document built for type checking declared `server` from
ast.dataApis-adjacent runtime functions but never declared `api`,
so every api.<name>(...) call raised a false 'Cannot find name apis'
plus a knock-on implicit-any on its result. Fixes it by declaring
`api` from ast.dataApis, mirroring the existing `server` declaration:
each entry gets an input parameter shaped from its request
parameters/body fields (optional when the entry declares none) and a
Promise<any> return. The binding is only emitted when the page has an
apis { } block, so pages without one keep the legitimate 'Cannot find
name api' diagnostic and 'state api' stays legal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 18:51:19 +05:30
ClintchizandClaude Opus 5 861444b8a3 feat(language-server): flag the removed data blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 18:40:08 +05:30
ClintchizandClaude Opus 5 ba957e861e chore(ui): re-baseline the visual contract after the auth UI refinements
cd0dffa8 changed StrongPassword, TogglePassword and button but left the
hash manifest untouched, so check:ui-visual has been red since. The three
hashes here are exactly those components -- no unrelated drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:50:44 +05:30
ClintchizandClaude Opus 5 29febb2e0c feat(language-server): understand the api binding attribute
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 16:49:06 +05:30
262 changed files with 13092 additions and 714 deletions
+6
View File
@@ -13,6 +13,12 @@
], ],
"port": 3520 "port": 3520
}, },
{
"name": "sendline",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "D:/Company/sendline", "dev", "--port=3610"],
"port": 3610
},
{ {
"name": "component-showcase", "name": "component-showcase",
"runtimeExecutable": "bun", "runtimeExecutable": "bun",
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
bun-version: 1.3.14 bun-version: 1.4.0
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: 24 node-version: 24
+11
View File
@@ -2,6 +2,17 @@
## Unreleased ## Unreleased
- Added framework-owned authentication and authorization integration: lazy auth stores,
configured OAuth routes with cookie-bound transactions, production configuration validation,
default lifecycle roles, typed request users, declarative API permissions, page guards, and
consistent unauthorized/forbidden responses.
- Added reusable application primitives for typed route params, bounded cursor pagination,
owned-resource authorization, atomic database state transitions, queue batching and explicit
worker lifecycles.
- Expanded `@wrnexus/test` with official typed contexts, Bun-compatible fetch mocking and a
full-stack harness alias; made generated files deterministic and introduced `wrnexus check` as
the canonical build-before-check command for generated applications.
- Fixed `v.boolean()` coercion in `@wrnexus/validation` (`checkField` in both `src/index.ts` - Fixed `v.boolean()` coercion in `@wrnexus/validation` (`checkField` in both `src/index.ts`
and the browser mirror in `src/runtime.ts`): previously any string other than `"true"` or and the browser mirror in `src/runtime.ts`): previously any string other than `"true"` or
`"on"` silently coerced to `false` with no error, so typos and unrecognised values (e.g. `"on"` silently coerced to `false` with no error, so typos and unrecognised values (e.g.
+143 -108
View File
@@ -9,7 +9,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/bun": "^1.3.14", "@types/bun": "^1.4.0",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.2.4",
"eslint": "^10.8.1", "eslint": "^10.8.1",
@@ -133,7 +133,7 @@
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"@wrnexus/cli": "^0.8.35", "@wrnexus/cli": "^0.8.36",
"eslint": "^10.8.1", "eslint": "^10.8.1",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -145,27 +145,27 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@wrnexus/ai": "^0.8.9", "@wrnexus/ai": "^0.8.9",
"@wrnexus/auth": "^0.8.11", "@wrnexus/auth": "^0.8.12",
"@wrnexus/authz": "^0.8.9", "@wrnexus/authz": "^0.8.9",
"@wrnexus/captcha": "^0.8.10", "@wrnexus/captcha": "^0.8.11",
"@wrnexus/core": "^0.8.9", "@wrnexus/core": "^0.8.9",
"@wrnexus/csr": "^0.8.21", "@wrnexus/csr": "^0.8.21",
"@wrnexus/db": "^0.8.14", "@wrnexus/db": "^0.8.15",
"@wrnexus/dev-server": "^0.8.32", "@wrnexus/dev-server": "^0.8.33",
"@wrnexus/encryption": "^0.8.8", "@wrnexus/encryption": "^0.8.9",
"@wrnexus/helpers": "^0.8.8", "@wrnexus/helpers": "^0.8.8",
"@wrnexus/i18n": "^0.8.10", "@wrnexus/i18n": "^0.8.11",
"@wrnexus/image": "^0.8.9", "@wrnexus/image": "^0.8.10",
"@wrnexus/jwt": "^0.8.8", "@wrnexus/jwt": "^0.8.9",
"@wrnexus/observability": "^0.8.8", "@wrnexus/observability": "^0.8.8",
"@wrnexus/realtime": "^0.8.9", "@wrnexus/realtime": "^0.8.10",
"@wrnexus/security": "^0.8.8", "@wrnexus/security": "^0.8.8",
"@wrnexus/store": "^0.8.8", "@wrnexus/store": "^0.8.8",
"@wrnexus/styles": "^0.8.15", "@wrnexus/styles": "^0.8.15",
"@wrnexus/tracking": "^0.8.8", "@wrnexus/tracking": "^0.8.8",
"@wrnexus/ui": "^0.8.19", "@wrnexus/ui": "^0.8.19",
"@wrnexus/uploader": "^0.8.9", "@wrnexus/uploader": "^0.8.10",
"@wrnexus/validation": "^0.8.9", "@wrnexus/validation": "^0.8.10",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@@ -173,7 +173,7 @@
"@iconify/tailwind4": "^1.2.3", "@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3", "@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"@wrnexus/cli": "^0.8.35", "@wrnexus/cli": "^0.8.36",
"eslint": "^10.8.1", "eslint": "^10.8.1",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
@@ -186,27 +186,27 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@wrnexus/ai": "^0.8.9", "@wrnexus/ai": "^0.8.9",
"@wrnexus/auth": "^0.8.11", "@wrnexus/auth": "^0.8.12",
"@wrnexus/authz": "^0.8.9", "@wrnexus/authz": "^0.8.9",
"@wrnexus/captcha": "^0.8.10", "@wrnexus/captcha": "^0.8.11",
"@wrnexus/core": "^0.8.9", "@wrnexus/core": "^0.8.9",
"@wrnexus/csr": "^0.8.21", "@wrnexus/csr": "^0.8.21",
"@wrnexus/db": "^0.8.14", "@wrnexus/db": "^0.8.15",
"@wrnexus/dev-server": "^0.8.32", "@wrnexus/dev-server": "^0.8.33",
"@wrnexus/encryption": "^0.8.8", "@wrnexus/encryption": "^0.8.9",
"@wrnexus/helpers": "^0.8.8", "@wrnexus/helpers": "^0.8.8",
"@wrnexus/i18n": "^0.8.10", "@wrnexus/i18n": "^0.8.11",
"@wrnexus/image": "^0.8.9", "@wrnexus/image": "^0.8.10",
"@wrnexus/jwt": "^0.8.8", "@wrnexus/jwt": "^0.8.9",
"@wrnexus/observability": "^0.8.8", "@wrnexus/observability": "^0.8.8",
"@wrnexus/realtime": "^0.8.9", "@wrnexus/realtime": "^0.8.10",
"@wrnexus/security": "^0.8.8", "@wrnexus/security": "^0.8.8",
"@wrnexus/store": "^0.8.8", "@wrnexus/store": "^0.8.8",
"@wrnexus/styles": "^0.8.15", "@wrnexus/styles": "^0.8.15",
"@wrnexus/tracking": "^0.8.8", "@wrnexus/tracking": "^0.8.8",
"@wrnexus/ui": "^0.8.19", "@wrnexus/ui": "^0.8.19",
"@wrnexus/uploader": "^0.8.9", "@wrnexus/uploader": "^0.8.10",
"@wrnexus/validation": "^0.8.9", "@wrnexus/validation": "^0.8.10",
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@@ -214,7 +214,7 @@
"@iconify/tailwind4": "^1.2.3", "@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3", "@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"@wrnexus/cli": "^0.8.35", "@wrnexus/cli": "^0.8.36",
"eslint": "^10.8.1", "eslint": "^10.8.1",
"prettier": "^3.9.6", "prettier": "^3.9.6",
"tailwindcss": "^4.3.3", "tailwindcss": "^4.3.3",
@@ -235,7 +235,7 @@
}, },
"packages/auth": { "packages/auth": {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.13", "version": "0.8.18",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*", "@wrnexus/captcha": "workspace:*",
@@ -257,10 +257,11 @@
}, },
"packages/authz": { "packages/authz": {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.8.9", "version": "0.8.15",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*", "@wrnexus/db": "workspace:*",
"@wrnexus/plugin": "workspace:*",
}, },
}, },
"packages/benchmark": { "packages/benchmark": {
@@ -276,7 +277,7 @@
}, },
"packages/captcha": { "packages/captcha": {
"name": "@wrnexus/captcha", "name": "@wrnexus/captcha",
"version": "0.8.11", "version": "0.8.14",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -291,7 +292,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.47", "version": "0.8.64",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -319,7 +320,7 @@
}, },
"packages/compiler": { "packages/compiler": {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.14", "version": "0.8.22",
"dependencies": { "dependencies": {
"@wrnexus/csr": "workspace:*", "@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*", "@wrnexus/store": "workspace:*",
@@ -337,18 +338,18 @@
}, },
"packages/core": { "packages/core": {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.8.10", "version": "0.8.12",
}, },
"packages/csr": { "packages/csr": {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.8.25", "version": "0.8.35",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/db": { "packages/db": {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.8.16", "version": "0.8.20",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -356,7 +357,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.42", "version": "0.8.56",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
@@ -370,6 +371,8 @@
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
"@wrnexus/pubsub": "workspace:*", "@wrnexus/pubsub": "workspace:*",
"@wrnexus/pwa": "workspace:*", "@wrnexus/pwa": "workspace:*",
"@wrnexus/queue": "workspace:*",
"@wrnexus/react": "workspace:*",
"@wrnexus/router": "workspace:*", "@wrnexus/router": "workspace:*",
"@wrnexus/rpc": "workspace:*", "@wrnexus/rpc": "workspace:*",
"@wrnexus/security": "workspace:*", "@wrnexus/security": "workspace:*",
@@ -383,7 +386,7 @@
}, },
"packages/dev-toolbar": { "packages/dev-toolbar": {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.8.13", "version": "0.8.16",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -391,9 +394,10 @@
}, },
"packages/encryption": { "packages/encryption": {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.8.9", "version": "0.8.10",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
@@ -409,9 +413,11 @@
}, },
"packages/helpers": { "packages/helpers": {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.8.8", "version": "0.8.9",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
}, },
}, },
"packages/i18n": { "packages/i18n": {
@@ -470,7 +476,7 @@
}, },
"packages/language-server": { "packages/language-server": {
"name": "@wrnexus/language-server", "name": "@wrnexus/language-server",
"version": "0.8.11", "version": "0.8.12",
"bin": { "bin": {
"wrnexus-language-server": "src/server.ts", "wrnexus-language-server": "src/server.ts",
}, },
@@ -480,6 +486,14 @@
"vscode-html-languageservice": "^5.6.2", "vscode-html-languageservice": "^5.6.2",
}, },
}, },
"packages/mail": {
"name": "@wrnexus/mail",
"version": "0.8.3",
"dependencies": {
"@wrnexus/encryption": "workspace:*",
"@wrnexus/queue": "workspace:*",
},
},
"packages/mcp": { "packages/mcp": {
"name": "@wrnexus/mcp", "name": "@wrnexus/mcp",
"version": "0.8.8", "version": "0.8.8",
@@ -487,6 +501,10 @@
"wrnexus-mcp": "src/stdio.ts", "wrnexus-mcp": "src/stdio.ts",
}, },
}, },
"packages/metering": {
"name": "@wrnexus/metering",
"version": "0.8.3",
},
"packages/mobile": { "packages/mobile": {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.8.8", "version": "0.8.8",
@@ -512,6 +530,14 @@
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/payment": {
"name": "@wrnexus/payment",
"version": "0.8.1",
"dependencies": {
"@wrnexus/db": "workspace:*",
"@wrnexus/plugin": "workspace:*",
},
},
"packages/playground": { "packages/playground": {
"name": "@wrnexus/playground", "name": "@wrnexus/playground",
"version": "0.8.8", "version": "0.8.8",
@@ -545,9 +571,10 @@
}, },
"packages/queue": { "packages/queue": {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.8.8", "version": "0.8.14",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/rpc": "workspace:*", "@wrnexus/rpc": "workspace:*",
}, },
}, },
@@ -572,7 +599,7 @@
}, },
"packages/reactive": { "packages/reactive": {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.8.8", "version": "0.8.9",
}, },
"packages/realtime": { "packages/realtime": {
"name": "@wrnexus/realtime", "name": "@wrnexus/realtime",
@@ -590,7 +617,7 @@
}, },
"packages/router": { "packages/router": {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.8.9", "version": "0.8.10",
"dependencies": { "dependencies": {
"@wrnexus/compiler": "workspace:*", "@wrnexus/compiler": "workspace:*",
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
@@ -612,7 +639,7 @@
}, },
"packages/security": { "packages/security": {
"name": "@wrnexus/security", "name": "@wrnexus/security",
"version": "0.8.8", "version": "0.8.9",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
@@ -628,11 +655,11 @@
}, },
"packages/store": { "packages/store": {
"name": "@wrnexus/store", "name": "@wrnexus/store",
"version": "0.8.8", "version": "0.8.12",
}, },
"packages/styles": { "packages/styles": {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.8.15", "version": "0.8.20",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -641,11 +668,11 @@
}, },
"packages/syntax": { "packages/syntax": {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.8.10", "version": "0.8.18",
}, },
"packages/test": { "packages/test": {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.8.9", "version": "0.8.12",
}, },
"packages/tracking": { "packages/tracking": {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
@@ -653,7 +680,7 @@
}, },
"packages/typecheck": { "packages/typecheck": {
"name": "@wrnexus/typecheck", "name": "@wrnexus/typecheck",
"version": "0.8.10", "version": "0.8.16",
"dependencies": { "dependencies": {
"@wrnexus/syntax": "workspace:*", "@wrnexus/syntax": "workspace:*",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -661,14 +688,14 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.21", "version": "0.8.56",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/uploader": { "packages/uploader": {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.8.10", "version": "0.8.13",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -769,7 +796,7 @@
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "@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=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
@@ -795,7 +822,7 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], "@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=="], "@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=="],
@@ -815,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=="], "@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=="], "@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=="], "@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=="],
@@ -845,79 +874,79 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="], "@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/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=="], "@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=="], "@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=="],
@@ -949,7 +978,7 @@
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="], "@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=="], "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
@@ -957,11 +986,11 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@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": ["@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=="], "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
@@ -1033,10 +1062,14 @@
"@wrnexus/language-server": ["@wrnexus/language-server@workspace:packages/language-server"], "@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/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"],
"@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"], "@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"],
"@wrnexus/metering": ["@wrnexus/metering@workspace:packages/metering"],
"@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"], "@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"],
"@wrnexus/native": ["@wrnexus/native@workspace:packages/native"], "@wrnexus/native": ["@wrnexus/native@workspace:packages/native"],
@@ -1045,6 +1078,8 @@
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"], "@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
"@wrnexus/payment": ["@wrnexus/payment@workspace:packages/payment"],
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"], "@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"], "@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
@@ -1087,7 +1122,7 @@
"@wrnexus/validation": ["@wrnexus/validation@workspace:packages/validation"], "@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=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
@@ -1113,7 +1148,7 @@
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], "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=="], "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
@@ -1145,7 +1180,7 @@
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "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=="], "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -1167,7 +1202,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "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=="], "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=="],
@@ -1203,7 +1238,7 @@
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], "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=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -1211,7 +1246,7 @@
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "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"], "i18n-showcase": ["i18n-showcase@workspace:examples/i18n-showcase"],
@@ -1283,7 +1318,7 @@
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "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=="], "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=="],
@@ -1319,7 +1354,7 @@
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "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=="], "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
@@ -1337,7 +1372,7 @@
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], "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=="], "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
@@ -1349,9 +1384,9 @@
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "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=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
@@ -1427,27 +1462,27 @@
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"], "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=="], "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=="], "@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/@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=="], "@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=="], "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
+229 -5
View File
@@ -91,6 +91,7 @@
"AuthPluginOptions", "AuthPluginOptions",
"AuthPublicUser", "AuthPublicUser",
"AuthRandom", "AuthRandom",
"AuthRequiredError",
"AuthResult", "AuthResult",
"AuthRiskDecision", "AuthRiskDecision",
"AuthRiskLevel", "AuthRiskLevel",
@@ -210,6 +211,7 @@
"signUpSchema", "signUpSchema",
"totpUri", "totpUri",
"tryGetDefaultAuthEngine", "tryGetDefaultAuthEngine",
"validateProductionAuthConfig",
"verificationRequestSchema", "verificationRequestSchema",
"verificationTokenSchema", "verificationTokenSchema",
"verifyTotp" "verifyTotp"
@@ -229,6 +231,7 @@
], ],
"./middleware": [ "./middleware": [
"AUTH_SESSION_KEY", "AUTH_SESSION_KEY",
"AuthRequiredError",
"AuthSessionOptions", "AuthSessionOptions",
"RequireAuthOptions", "RequireAuthOptions",
"authSession", "authSession",
@@ -237,7 +240,8 @@
"getAuthSession", "getAuthSession",
"getAuthUser", "getAuthUser",
"isAuthenticatedContext", "isAuthenticatedContext",
"requireAuth" "requireAuth",
"requireAuthUser"
], ],
"./passkeys": [ "./passkeys": [
"MemoryPasskeyChallengeStore", "MemoryPasskeyChallengeStore",
@@ -252,11 +256,13 @@
"./plugin": [ "./plugin": [
"AuthAuditIssue", "AuthAuditIssue",
"AuthConfig", "AuthConfig",
"AuthOAuthProviderConfig",
"AuthPluginOptions", "AuthPluginOptions",
"AuthRoutesConfig", "AuthRoutesConfig",
"authComponentsDir", "authComponentsDir",
"authPlugin", "authPlugin",
"default" "default",
"validateProductionAuthConfig"
], ],
"./protector": [ "./protector": [
"createAuthSecretProtector" "createAuthSecretProtector"
@@ -460,7 +466,9 @@
"AUTHZ_LOCALS_KEY", "AUTHZ_LOCALS_KEY",
"AttributeMeta", "AttributeMeta",
"AuthorizationDecision", "AuthorizationDecision",
"AuthorizationResponses",
"AuthorizeDecisionOptions", "AuthorizeDecisionOptions",
"AuthorizedHandlerOptions",
"AuthzAuditEvent", "AuthzAuditEvent",
"AuthzAuditSink", "AuthzAuditSink",
"AuthzCatalog", "AuthzCatalog",
@@ -476,10 +484,12 @@
"GrantEffect", "GrantEffect",
"GuardOptions", "GuardOptions",
"MemoryAuditSink", "MemoryAuditSink",
"OwnedResourceDefinition",
"PermissionMeta", "PermissionMeta",
"PermissionStore", "PermissionStore",
"Policy", "Policy",
"Rbac", "Rbac",
"RequestAuthorization",
"Subject", "Subject",
"SubjectAssignments", "SubjectAssignments",
"all", "all",
@@ -497,7 +507,9 @@
"createAuthzResolver", "createAuthzResolver",
"decideFor", "decideFor",
"decision", "decision",
"defineAuthorizedHandler",
"defineAuthz", "defineAuthz",
"defineOwnedResource",
"defineRbac", "defineRbac",
"deniedBy", "deniedBy",
"deny", "deny",
@@ -507,6 +519,7 @@
"filterCan", "filterCan",
"generatePermissionTypes", "generatePermissionTypes",
"getAuthzCatalog", "getAuthzCatalog",
"getRequestAuthorization",
"guardPermission", "guardPermission",
"hasAuthzCatalog", "hasAuthzCatalog",
"hasRole", "hasRole",
@@ -525,6 +538,23 @@
"authzMigrationSql", "authzMigrationSql",
"dbPermissionStore", "dbPermissionStore",
"ensureAuthzTables" "ensureAuthzTables"
],
"./defaults": [
"AuthzIdentityEvent",
"assignDefaultAuthzRoles",
"setDefaultAuthzRoleStore",
"setDefaultAuthzRoles"
],
"./plugin": [
"AuthzConfig",
"authzPlugin",
"default"
],
"./runtime-memory-middleware": [
"default"
],
"./runtime-middleware": [
"default"
] ]
}, },
"@wrnexus/benchmark": { "@wrnexus/benchmark": {
@@ -938,6 +968,7 @@
}, },
"@wrnexus/cli": { "@wrnexus/cli": {
".": [], ".": [],
"./test-setup": [],
"./workspace": [ "./workspace": [
"ResolvedWorkspaceApp", "ResolvedWorkspaceApp",
"ResolvedWorkspaceConfig", "ResolvedWorkspaceConfig",
@@ -1137,6 +1168,8 @@
"POSTGRES_TENANT_DIRECTORY_SCHEMA", "POSTGRES_TENANT_DIRECTORY_SCHEMA",
"PageComponent", "PageComponent",
"PageMeta", "PageMeta",
"PaginationInput",
"PaginationOptions",
"PerformanceBudgets", "PerformanceBudgets",
"PerformanceMeasurement", "PerformanceMeasurement",
"PermissionsPolicyConfig", "PermissionsPolicyConfig",
@@ -1198,6 +1231,7 @@
"TenantResource", "TenantResource",
"TenantSqlClient", "TenantSqlClient",
"Tracer", "Tracer",
"TransactionCookie",
"TrustedTypesConfig", "TrustedTypesConfig",
"UploadError", "UploadError",
"UploadInspectionResult", "UploadInspectionResult",
@@ -1319,6 +1353,8 @@
"ActionClientError", "ActionClientError",
"ActionClientOptions", "ActionClientOptions",
"ActionResult", "ActionResult",
"ApiClientOptions",
"ApiRequest",
"ClientModuleScope", "ClientModuleScope",
"HydrationScopeApi", "HydrationScopeApi",
"NAV_RUNTIME", "NAV_RUNTIME",
@@ -1332,6 +1368,7 @@
"callServerFunction", "callServerFunction",
"collectRefs", "collectRefs",
"createActionClient", "createActionClient",
"createApiClient",
"createOutputProxy", "createOutputProxy",
"createServerProxy", "createServerProxy",
"getActionRuntime", "getActionRuntime",
@@ -1342,11 +1379,13 @@
"invalidateClientModule", "invalidateClientModule",
"invokeOutput", "invokeOutput",
"loadClientFunctions", "loadClientFunctions",
"registerOutputHandler" "registerOutputHandler",
"useFetch"
] ]
}, },
"@wrnexus/db": { "@wrnexus/db": {
".": [ ".": [
"AddSeedOptions",
"BaseType", "BaseType",
"Column", "Column",
"ColumnDef", "ColumnDef",
@@ -1357,6 +1396,7 @@
"Dialect", "Dialect",
"Driver", "Driver",
"ExecResult", "ExecResult",
"LedgerOptions",
"Migration", "Migration",
"MigrationRunOptions", "MigrationRunOptions",
"MigrationSafetyIssue", "MigrationSafetyIssue",
@@ -1373,13 +1413,20 @@
"RelationOptions", "RelationOptions",
"Repository", "Repository",
"Row", "Row",
"SeedDatabase",
"SeedRow",
"SeedStep",
"SeedUserAccount",
"TxHandle", "TxHandle",
"UpsertSeedOptions",
"addSeedData",
"analyzeMigrationSafety", "analyzeMigrationSafety",
"analyzeMigrations", "analyzeMigrations",
"appliedMigrations", "appliedMigrations",
"applyMigrations", "applyMigrations",
"batch", "batch",
"closeDatabases", "closeDatabases",
"compareAndSet",
"countRows", "countRows",
"createDb", "createDb",
"createRepository", "createRepository",
@@ -1387,6 +1434,8 @@
"cursorPaginate", "cursorPaginate",
"databaseHealth", "databaseHealth",
"databaseNames", "databaseNames",
"defineLedger",
"defineSeed",
"exists", "exists",
"firstOrThrow", "firstOrThrow",
"generateQueriesFile", "generateQueriesFile",
@@ -1404,15 +1453,20 @@
"queryOperation", "queryOperation",
"registerDb", "registerDb",
"registerLazyDb", "registerLazyDb",
"removeSeedData",
"resetDbPerformanceSnapshot", "resetDbPerformanceSnapshot",
"retryTransaction", "retryTransaction",
"rollback", "rollback",
"runSeedQuery",
"scaffoldMigration", "scaffoldMigration",
"seedIfMissing",
"seedUsers",
"setDb", "setDb",
"softDeleteClause", "softDeleteClause",
"status", "status",
"table", "table",
"tenantScope", "tenantScope",
"upsertSeedData",
"v", "v",
"withTransaction" "withTransaction"
], ],
@@ -1613,6 +1667,7 @@
"EncryptionKey", "EncryptionKey",
"EncryptionKeyring", "EncryptionKeyring",
"ReplayStore", "ReplayStore",
"SecretResourceOptions",
"createEncryptedRequest", "createEncryptedRequest",
"createKeyring", "createKeyring",
"createMemoryReplayStore", "createMemoryReplayStore",
@@ -1620,6 +1675,7 @@
"decryptEncryptedResponse", "decryptEncryptedResponse",
"decryptHttpBody", "decryptHttpBody",
"decryptRequest", "decryptRequest",
"defineSecretResource",
"deriveKey", "deriveKey",
"encrypt", "encrypt",
"encryptHttpBody", "encryptHttpBody",
@@ -1653,26 +1709,39 @@
"@wrnexus/helpers": { "@wrnexus/helpers": {
".": [ ".": [
"AllowedHosts", "AllowedHosts",
"ApiHandler",
"HttpError",
"LoginRedirectOptions", "LoginRedirectOptions",
"OriginalRequestOptions", "OriginalRequestOptions",
"RequestContext", "RequestContext",
"ResourceDefinition",
"ResourceHandlers",
"RetryOptions", "RetryOptions",
"appOrigin", "appOrigin",
"appUrl", "appUrl",
"authorized",
"backoffDelay", "backoffDelay",
"clamp", "clamp",
"currentAppName", "currentAppName",
"currentAppOrigin", "currentAppOrigin",
"defineApiRoute",
"defineResource",
"getOriginalRequestMethod", "getOriginalRequestMethod",
"getOriginalRequestOrigin", "getOriginalRequestOrigin",
"getOriginalRequestPath", "getOriginalRequestPath",
"getOriginalRequestUrl", "getOriginalRequestUrl",
"json",
"once", "once",
"redirectToLogin", "redirectToLogin",
"requireJson",
"requireParam",
"requirePermission",
"requireUser",
"retry", "retry",
"safeJsonParse", "safeJsonParse",
"sleep", "sleep",
"stableStringify", "stableStringify",
"subjectId",
"withTimeout", "withTimeout",
"workspaceAppOrigins", "workspaceAppOrigins",
"workspaceRootDomain" "workspaceRootDomain"
@@ -1864,7 +1933,27 @@
"./server": [ "./server": [
"ApiCallCompletionItem", "ApiCallCompletionItem",
"apiCallCompletions", "apiCallCompletions",
"apiCallHover" "apiCallHover",
"isApiReferenceAt"
]
},
"@wrnexus/mail": {
".": [
"MailDriver",
"MailMessage",
"MailOptions",
"MailSandboxDecision",
"MailTemplate",
"SealedMailCredentialsOptions",
"SealedMailDriverOptions",
"defineMail",
"defineMailTemplate",
"defineSealedMailCredentials",
"interpolateTemplate",
"mailSandboxDecision",
"queuedMail",
"sealedMailDriver",
"storedMailTemplate"
] ]
}, },
"@wrnexus/mcp": { "@wrnexus/mcp": {
@@ -1878,6 +1967,20 @@
"runMcpStdio" "runMcpStdio"
] ]
}, },
"@wrnexus/metering": {
".": [
"EntitlementPlan",
"EntitlementsOptions",
"MeterKind",
"MeterOptions",
"MeterPack",
"MeterResult",
"MeterStore",
"defineEntitlements",
"defineMeter",
"definePacks"
]
},
"@wrnexus/mobile": { "@wrnexus/mobile": {
".": [ ".": [
"CapacitorBridge", "CapacitorBridge",
@@ -2078,6 +2181,70 @@
"traceMiddleware" "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": { "@wrnexus/playground": {
".": [ ".": [
"PlaygroundCompilation", "PlaygroundCompilation",
@@ -2242,23 +2409,44 @@
"@wrnexus/queue": { "@wrnexus/queue": {
".": [ ".": [
"AddOptions", "AddOptions",
"DefineQueueOptions",
"DefinedJob",
"DefinedJobOptions",
"DefinedJobs",
"DefinedQueue",
"DurableQueue", "DurableQueue",
"DurableQueueOptions", "DurableQueueOptions",
"Job", "Job",
"JobContext", "JobContext",
"JobDefinition", "JobDefinition",
"JobHandler", "JobHandler",
"JobStateMachineOptions",
"JobStatus",
"OutboxEntry",
"OutboxWriter",
"POSTGRES_QUEUE_SCHEMA", "POSTGRES_QUEUE_SCHEMA",
"Queue", "Queue",
"QueueAdminOptions",
"QueueDashboardSnapshot", "QueueDashboardSnapshot",
"QueueEvent",
"QueueEventType",
"QueueHealth",
"QueueJobDefinitions",
"QueueJobRecord",
"QueueJobState",
"QueueOptions", "QueueOptions",
"QueueScheduler", "QueueScheduler",
"QueueStorage",
"QueueStorageConfig",
"QueueStore", "QueueStore",
"RedisQueueClient", "RedisQueueClient",
"SagaStep",
"ScheduledJob", "ScheduledJob",
"SqlQueueClient", "SqlQueueClient",
"SqliteQueueClient",
"SubjectJob", "SubjectJob",
"SubjectQueue", "SubjectQueue",
"WorkerDefinition",
"WorkflowDefinition", "WorkflowDefinition",
"WorkflowEngine", "WorkflowEngine",
"WorkflowRunContext", "WorkflowRunContext",
@@ -2267,22 +2455,46 @@
"WorkflowStep", "WorkflowStep",
"WorkflowStore", "WorkflowStore",
"addBatch", "addBatch",
"configureQueueStorage",
"configuredQueueStore",
"createDurableQueue", "createDurableQueue",
"createQueue", "createQueue",
"createQueueAdminHandler",
"createQueueScheduler", "createQueueScheduler",
"createWorkflowEngine", "createWorkflowEngine",
"cronToInterval", "cronToInterval",
"databaseQueueStore",
"defineDurableWorkflow", "defineDurableWorkflow",
"defineJob", "defineJob",
"defineJobStateMachine",
"defineQueue",
"defineSaga",
"defineWorker",
"defineWorkflow", "defineWorkflow",
"drainOutbox",
"installDatabaseQueueSchema",
"installOutboxSchema",
"installSqliteQueueSchema",
"memoryQueueStore", "memoryQueueStore",
"memoryWorkflowStore", "memoryWorkflowStore",
"postgresQueueStore", "postgresQueueStore",
"queueDashboardSnapshot", "queueDashboardSnapshot",
"queueStorageConfig",
"redisQueueStore", "redisQueueStore",
"renderQueueDashboard", "renderQueueDashboard",
"runQueueDaemon", "runQueueDaemon",
"subjectQueue" "runWorker",
"sqliteQueueSchema",
"sqliteQueueStore",
"subjectQueue",
"testQueue",
"withOutbox"
],
"./sqlite": [
"SqliteQueueClient",
"installSqliteQueueSchema",
"sqliteQueueSchema",
"sqliteQueueStore"
] ]
}, },
"@wrnexus/react": { "@wrnexus/react": {
@@ -2325,6 +2537,7 @@
".": [ ".": [
"AnimationTimeline", "AnimationTimeline",
"Cleanup", "Cleanup",
"FormState",
"HistorySignal", "HistorySignal",
"ReactiveContext", "ReactiveContext",
"ReactiveScope", "ReactiveScope",
@@ -2351,6 +2564,7 @@
"transition", "transition",
"untrack", "untrack",
"urlSignal", "urlSignal",
"useForm",
"watch" "watch"
] ]
}, },
@@ -2638,6 +2852,7 @@
".": [ ".": [
"ACCENT_COOKIE", "ACCENT_COOKIE",
"AppConfig", "AppConfig",
"AppConfigInput",
"BrowserCookieApi", "BrowserCookieApi",
"BrowserCookieOptions", "BrowserCookieOptions",
"BrowserCookiePreference", "BrowserCookiePreference",
@@ -2662,6 +2877,7 @@
"ObservabilityConfig", "ObservabilityConfig",
"PerformanceConfig", "PerformanceConfig",
"PwaConfig", "PwaConfig",
"ResolvedAppConfig",
"ResolvedConfigLayers", "ResolvedConfigLayers",
"ResolvedTheme", "ResolvedTheme",
"StyleProcessContext", "StyleProcessContext",
@@ -2876,10 +3092,14 @@
".": [ ".": [
"BrowserArtifactPage", "BrowserArtifactPage",
"Deferred", "Deferred",
"DetectMutationsOptions",
"FetchMock",
"Harness", "Harness",
"HarnessOptions", "HarnessOptions",
"JsonResponse", "JsonResponse",
"MemoryCookieJar", "MemoryCookieJar",
"MutationCase",
"MutationReport",
"TestRequestOptions", "TestRequestOptions",
"TransactionalDatabase", "TransactionalDatabase",
"WaitForOptions", "WaitForOptions",
@@ -2891,9 +3111,13 @@
"captureBrowserArtifacts", "captureBrowserArtifacts",
"createContext", "createContext",
"createFactory", "createFactory",
"createFetchMock",
"createHarness", "createHarness",
"createTestApp",
"createTestContext",
"deferred", "deferred",
"describe", "describe",
"detectMutations",
"expect", "expect",
"expectProblem", "expectProblem",
"it", "it",
+32 -32
View File
@@ -4,22 +4,22 @@
"artifacts": { "artifacts": {
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b", "packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b", "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/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5", "packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97", "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/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a", "packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72", "packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
"packages/ui/components/CTASection.wrn": "d0665f2aa33eea95e6f84120f739b3dd9c8723c882138bcb1a7f80dcd7909904", "packages/ui/components/CTASection.wrn": "f6872b980432d5ee9d88388250be9838ccce51bd706349b283ef472ee692b237",
"packages/ui/components/Card.wrn": "3cc2ff238bda279c169026a462236144ad11ec0046deef00ab00e001a07e8a4c", "packages/ui/components/Card.wrn": "be8cd6a438d1ede4c284a302b76f0fbc235a2423b83a7ced21131763fecc8a90",
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9", "packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4", "packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1", "packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
"packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30", "packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30",
"packages/ui/components/Collapse.wrn": "26df087a61c82f1aa1749785f4c9be654c4f934859c1a47acfcb8cc67eecc75d", "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/Columns.wrn": "8ccd9e53f6799b09f8b996740d12e4e73ee782f6fd3e57595c6c16f788b9b812",
"packages/ui/components/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1", "packages/ui/components/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1",
"packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e", "packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e",
@@ -28,7 +28,7 @@
"packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437", "packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437",
"packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3", "packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3",
"packages/ui/components/DataTable.wrn": "e929fdb4e59613878a03fb6a11e09c42f773c06ca7b20e43ae16a266835f2af4", "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/DeviceFrame.wrn": "c38d25419ee44b05d7c443cc4e854aeeb7a53a9289e374d71834de2dd2e4d979",
"packages/ui/components/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019", "packages/ui/components/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019",
"packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513", "packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513",
@@ -36,15 +36,15 @@
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0", "packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced", "packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897", "packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
"packages/ui/components/FeatureIconCard.wrn": "3b4f3fa62c6729e686886a5b848e26c255766684829cec2b715967b4e73d6f07", "packages/ui/components/FeatureIconCard.wrn": "5f65d64235babc3e653a5695204412202015e6f3ffadca566fc18c7f9ce70428",
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8", "packages/ui/components/FileInput.wrn": "611047c362458a4e2513b741cc3ef43fbe54c10ddafc13775e4ff39c9de34931",
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034", "packages/ui/components/FileUploadProgress.wrn": "ba00557afebf60e6221385d6efcf08635789328deee6e551fc53fbb71e1f413b",
"packages/ui/components/Footer.wrn": "f7c5a77064a09e244f689d42475c4c20143f4c8859cac451c10a50c14652db33", "packages/ui/components/Footer.wrn": "e3515e05ef3ef51e6dad71d67ddfe110c80fdcf69e952aecdc7973e69d4f08c7",
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517", "packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900", "packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790", "packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
"packages/ui/components/Image.wrn": "361179c478d7674c3b998891a366d73de9514fc6cc8786440c6c50b9ef065357", "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/InputNumber.wrn": "069195cc483f3f7d6f305d03fbe5a821906a030376fafb6f0d969e4c95b68fd0",
"packages/ui/components/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9", "packages/ui/components/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9",
"packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1", "packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1",
@@ -55,54 +55,54 @@
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890", "packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad", "packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b", "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/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
"packages/ui/components/MetricGrid.wrn": "c71e53249835908833055b553e64a8ee55fdf11ac4605436a9581ebb87a0608b", "packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2", "packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a", "packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
"packages/ui/components/Navbar.wrn": "01903230210cd2e5e6d9325a0708f85af9623d4118e3a53f1adb770fd545425d", "packages/ui/components/Navbar.wrn": "b68a824e479ce6957afc3e9710442281cdc1a8d02cd96501fe060271f54368e4",
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54", "packages/ui/components/PageHeader.wrn": "29a93cdca7285548b79ebe5b6421eef6dbb4d5ae8be2508fa2d681e1068ba07f",
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39", "packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602", "packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019", "packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee", "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/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
"packages/ui/components/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b", "packages/ui/components/RangeSlider.wrn": "e8ab41782c9d6e58638f0bfccf6e3172af4ebf552189d2199cc6c96bd1652067",
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587", "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/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
"packages/ui/components/Section.wrn": "45b7a1977c25a6020d5cc8bab2ce9c80730e7ec2d51ad53e8c9f64cd9b5b64cd", "packages/ui/components/Section.wrn": "78da13eb8bc1acafe8e6151cbaeed4baaac2ab860e3209f1d13c467f279d2e68",
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5", "packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6", "packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
"packages/ui/components/SplitHero.wrn": "843387dc7b920b3cf6a1b2b785b8653c281982e524479edc2638a89381ff3577", "packages/ui/components/SplitHero.wrn": "7127cb869d1228522d066e028818dd9e512de8b6dd8f99e3dec500e6da5a3cb0",
"packages/ui/components/StatsBar.wrn": "c273bd6ca417a0a20e5e6f1214414f678741b73bedd885fea1037575fa8c5b95", "packages/ui/components/StatsBar.wrn": "6fabcb4374b1e074f58ebcaba968f235c8a3409762163aea51298e2515a14435",
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142", "packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
"packages/ui/components/StrongPassword.wrn": "d5ea906fafe268b070a26ab551a252c0575d899c9d2b03b337bd047688b85dc5", "packages/ui/components/StrongPassword.wrn": "cc31cb080c4b4116c544b4340b8d740d08fec1b05563372b3280c50759416c87",
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d", "packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9", "packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954", "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/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c", "packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f", "packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
"packages/ui/components/TogglePassword.wrn": "b6176453cb2c586fe88c3db6bdd734ae2b4c95db46d57e0de298dfba3d0dd0c2", "packages/ui/components/TogglePassword.wrn": "caa58479e5c8b1e3e504181a56a7d6bcf71d0a230010733710961c31e1cc58ab",
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b", "packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5", "packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
"packages/ui/components/WysiwygEditor.wrn": "3f9d1765184581e3c1f335769a9f19969917450258bc95ba076980d99f5610b5", "packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
"packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d", "packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d",
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc", "packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1", "packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
"packages/ui/components/button.wrn": "cba4ccbbd23bb75b3836ec7a53673e40f1e043d18bfcee3d613db96c318f4ba8", "packages/ui/components/button.wrn": "65570c58cb2547f1e17cd158eec7cf7b1ffc721e6976c6486c9df564627b1219",
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4", "packages/ui/components/checkbox.wrn": "da735354b57f33946591e1bd018b521ff709bae1325e2317735824673188377a",
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847", "packages/ui/components/input.wrn": "2f17353cbf0711ca50a935ae03ee9ee53594d7e35eb73f5df573fadb7958fac2",
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492", "packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
"packages/ui/components/radio.wrn": "09425a78358de5bbd2f47482f313e80065135335b969ce5ccdd5c7cc3ea5232c", "packages/ui/components/radio.wrn": "df8e562324b6d99f5a157b0971d63bd715daa0cf6189e7b0d4df04d6f0411388",
"packages/ui/components/select.wrn": "1d8d47a74d5ab9ed58d0ba11f46910897233b96652d17f5962f5dda60bd4cf8a", "packages/ui/components/select.wrn": "747321d6292b8052e44e8014e87ce25f4b56d8e07f86f1061e0ed0cec7651356",
"packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648", "packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648",
"packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105", "packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105",
"packages/ui/components/switch.wrn": "874504e4828e9db6a570d78984c4d3a76d0ceb39d70baa877649cb3798c82c85", "packages/ui/components/switch.wrn": "243f521431ffb644089df7f629bace0c07094638dfdd27e75b2017efa9cbba4f",
"packages/ui/components/textarea.wrn": "9870d37664471a103d44435a3977bf40b087a743acd76ee4d77df9b56cccbb2b", "packages/ui/components/textarea.wrn": "aa3a75214e998a0b57c50e416476d6f85fa0313aaf97262f2f9222150c6c9e26",
"packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d", "packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d",
"packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca", "packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca",
"packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62" "packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62"
+17
View File
@@ -1,5 +1,22 @@
# Changelog # Changelog
## 0.8.9
- Added highlighting for the page-level `apis { }` block and its `<name> <METHOD> <path>` entries,
along with container and entry completion snippets.
- Added `api.` call completion listing every declared block with its method and path, and hover
reporting a block's method, path, and request fields.
- Taught the language server the `api="…"` binding attribute: it is no longer treated as unknown,
and completion inside its quotes offers the page's block names.
- Diagnostics for the removed `ssr { … }` / `client { … }` data blocks now name the `apis { }`
replacement and land on the offending keyword instead of the start of the file.
- Fixed `api.<name>()` calls reporting a spurious "Cannot find name 'api'" error, and the implicit
`any` that followed from it.
- Restricted API hover to real `api.<name>` references, so a local variable that merely shares a
block's name keeps its own hover information.
- `client state { }`, `runtime = "client"`, and the `client function` modifier are unaffected —
only the `ssr { }` / `client { }` data-block form was removed.
## 0.8.8 ## 0.8.8
- Added HTML tag and attribute completions inside WRN `view` blocks, while preserving WRNexus - Added HTML tag and attribute completions inside WRN `view` blocks, while preserving WRNexus
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "wrnexus", "name": "wrnexus",
"displayName": "WRNexus Language Support", "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.", "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.8", "version": "0.8.11",
"publisher": "wrnexus", "publisher": "wrnexus",
"private": true, "private": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
+361 -40
View File
@@ -1,7 +1,7 @@
"use strict"; "use strict";
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly. // Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
// WRN editor compiler source hash: 2987e8e9d91b793818a195395e740d35d0d5c2a320ff1c3a5ae77363850c7b8e // WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8 // WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5
// Generated with TypeScript: 6.0.3 // Generated with TypeScript: 6.0.3
const __nodeRequire = require; const __nodeRequire = require;
const __path = __nodeRequire("node:path"); const __path = __nodeRequire("node:path");
@@ -1017,6 +1017,27 @@ const HTML_BOOLEAN_ATTRIBUTES = new Set([
function isHtmlBooleanAttribute(name) { function isHtmlBooleanAttribute(name) {
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase()); return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
} }
/*
* Constraint attributes that must be absent rather than empty. `pattern` is the
* dangerous one: an empty pattern compiles to a regex matching only the empty
* string, so every typed value becomes invalid and the form silently refuses to
* submit. A component declaring `pattern: string = ""` and rendering
* `pattern="{pattern}"` therefore breaks every input that did not ask for a
* pattern. The rest are inert when empty, but carry no meaning either.
*/
const OMIT_WHEN_EMPTY_ATTRIBUTES = new Set([
"pattern",
"minlength",
"maxlength",
"min",
"max",
"step",
"inputmode",
"accept",
]);
function isOmitWhenEmptyAttribute(name) {
return OMIT_WHEN_EMPTY_ATTRIBUTES.has(name.toLowerCase());
}
const URL_ATTRIBUTES = new Set([ const URL_ATTRIBUTES = new Set([
"href", "href",
"src", "src",
@@ -1078,9 +1099,13 @@ function renderAttr(attr) {
case "csrText": case "csrText":
return ""; return "";
default: default:
return attr.boolean if (attr.boolean)
? ` ${attr.name}` return ` ${attr.name}`;
: ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`; // An empty constraint attribute is worse than absent -- see
// OMIT_WHEN_EMPTY_ATTRIBUTES.
if (attr.value === "" && isOmitWhenEmptyAttribute(attr.name))
return "";
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
} }
} }
function eventAttribute(name) { function eventAttribute(name) {
@@ -1123,6 +1148,31 @@ function componentEventAttribute(name) {
} }
return `data-wrn-out-${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) { function reactiveAttrValue(raw, reactive) {
let found = false; let found = false;
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => { const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
@@ -1141,6 +1191,7 @@ function reactiveAttrValue(raw, reactive) {
return found ? value : null; return found ? value : null;
} }
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) { function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
attrs = expandBindings(attrs);
let bindIndex = 0; let bindIndex = 0;
const rendered = attrs const rendered = attrs
.map((attr) => { .map((attr) => {
@@ -1266,18 +1317,18 @@ function bakeLoopAttr(raw, typed = false) {
return out + escLit(attrEscape(raw.slice(last))); return out + escLit(attrEscape(raw.slice(last)));
} }
/** Render one loop-body node to template-literal source (nested loops inline). */ /** Render one loop-body node to template-literal source (nested loops inline). */
function renderLoopBody(node) { function renderLoopBody(node, locals = []) {
if (node.type === "text") { if (node.type === "text") {
return bakeLoopText(node.value); return bakeLoopText(node.value);
} }
if (node.type === "each") { if (node.type === "each") {
return compileEachExpr(node); return compileEachExpr(node, locals);
} }
if (node.type === "if") { if (node.type === "if") {
return compileIfExpr(node); return compileIfExpr(node, locals);
} }
const componentTag = isComponentTag(node.tag); const componentTag = isComponentTag(node.tag);
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component") .filter((attr) => attr.name !== "data-component")
.map((attr) => { .map((attr) => {
const name = attr.event const name = attr.event
@@ -1291,7 +1342,13 @@ function renderLoopBody(node) {
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`); return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
}) })
.join(""); .join("");
const inner = node.children.map(renderLoopBody).join(""); // A handler expression is emitted as text and evaluated at event time, so any
// `{#each}` variable it names has to travel with the element. The runtime
// resolves them with closest("[data-wrn-loop-locals]"). Only elements that
// actually bind an event need it -- marking every node would bloat the HTML.
const localsAttr = locals.length > 0 && node.attrs.some((attr) => attr.event) ? loopLocalsAttr(locals) : "";
const inner = node.children.map((child) => renderLoopBody(child, locals)).join("");
const openAttrs = attrs + localsAttr;
if (node.tag === "Static") if (node.tag === "Static")
return inner; return inner;
if (node.tag === "Dynamic") if (node.tag === "Dynamic")
@@ -1335,26 +1392,37 @@ function renderLoopBody(node) {
if (island) if (island)
return escLit(island); return escLit(island);
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) + return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
attrs + openAttrs +
escLit(">") + escLit(">") +
inner + inner +
escLit("</div>")); escLit("</div>"));
} }
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) { if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return escLit(`<${node.tag}`) + attrs + escLit(">"); return escLit(`<${node.tag}`) + openAttrs + escLit(">");
} }
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`); return escLit(`<${node.tag}`) + openAttrs + escLit(">") + inner + escLit(`</${node.tag}>`);
}
/**
* Build the ` data-wrn-loop-locals="..."` attribute for a page-rendered loop
* body. Deliberately not passed through escLit: the `${...}` must stay live so
* the values are encoded at render time.
*/
function loopLocalsAttr(locals) {
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
} }
/** /**
* Compile a `{#each list as item}` block to a `${}` template-literal interpolation * Compile a `{#each list as item}` block to a `${}` template-literal interpolation
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a * that iterates the (server-evaluated) list and joins the per-item body. `list` is a
* JS expression evaluated where `ssr` data bindings are in scope as raw named values. * JS expression evaluated where `ssr` data bindings are in scope as raw named values.
*/ */
function compileEachExpr(node) { function compileEachExpr(node, outerLocals = []) {
const item = node.item; const item = node.item;
const index = node.index ?? "__wi"; const index = node.index ?? "__wi";
const body = node.body.map(renderLoopBody).join(""); // A nested loop can reference the outer loop's variables too.
const empty = node.empty.map(renderLoopBody).join(""); const locals = [...outerLocals, item, index];
const body = node.body.map((child) => renderLoopBody(child, locals)).join("");
const empty = node.empty.map((child) => renderLoopBody(child, outerLocals)).join("");
return ("${(() => { const __wl = Array.isArray(" + return ("${(() => { const __wl = Array.isArray(" +
node.list + node.list +
") ? (" + ") ? (" +
@@ -1374,11 +1442,11 @@ function compileEachExpr(node) {
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither). * that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
* Conditions are JS expressions evaluated in the surrounding server scope. * Conditions are JS expressions evaluated in the surrounding server scope.
*/ */
function compileIfExpr(node) { function compileIfExpr(node, locals = []) {
let expr = "``"; // no matching branch → empty string let expr = "``"; // no matching branch → empty string
for (let k = node.branches.length - 1; k >= 0; k--) { for (let k = node.branches.length - 1; k >= 0; k--) {
const b = node.branches[k]; const b = node.branches[k];
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`"; const bodySrc = "`" + b.body.map((child) => renderLoopBody(child, locals)).join("") + "`";
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr; expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
} }
return "${" + expr + "}"; return "${" + expr + "}";
@@ -1520,9 +1588,9 @@ function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindin
const island = islandMarkerFor(node); const island = islandMarkerFor(node);
if (island) if (island)
return island; return island;
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component") .filter((attr) => attr.name !== "data-component")
.map((attr) => renderPageComponentAttr(attr, loops)) .map((attr) => renderPageComponentAttr(attr, loops, reactive))
.join(""); .join("");
const inner = node.children const inner = node.children
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)) .map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
@@ -1535,7 +1603,7 @@ function renderNestedComponentInvocation(node, ctx) {
if (island) if (island)
return island; return island;
let bindIndex = 0; let bindIndex = 0;
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component") .filter((attr) => attr.name !== "data-component")
.map((attr) => { .map((attr) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name); const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
@@ -2257,6 +2325,18 @@ function generateInner(ast) {
if (needsRuntimeHelpers) { if (needsRuntimeHelpers) {
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`); out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
out.push(ssrRuntimeSource()); out.push(ssrRuntimeSource());
// Loop bodies that bind an event carry their {#each} locals in an encoded
// attribute. Emitted only when the view actually produced one, so a page
// without handlers in a loop keeps the smaller prelude -- but it MUST be
// emitted whenever the marker is, or the render throws on an undefined
// function instead of the handler throwing on an undefined variable.
if (body.includes("__wrnexusEncodeLoopLocals(")) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
const json = JSON.stringify(value);
const buffer = (globalThis as { Buffer?: { from(i: string, e: string): { toString(e: string): string } } }).Buffer;
return buffer ? buffer.from(json, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(json)));
}`);
}
out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`); out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`);
} }
if (hasServerApis) { if (hasServerApis) {
@@ -2392,6 +2472,8 @@ ${exposed
${declarations} ${declarations}
${visible.length ${visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]); ? ` 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(", ")} };` return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: ""} : ""}
}`; }`;
@@ -2711,7 +2793,7 @@ function renderClientControlTemplate(nodes) {
} }
const componentTag = isComponentTag(node.tag); const componentTag = isComponentTag(node.tag);
let bindIndex = 0; let bindIndex = 0;
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.map((attribute) => { .map((attribute) => {
const name = attribute.event const name = attribute.event
? componentTag ? componentTag
@@ -2749,19 +2831,21 @@ function encodeClientControl(value) {
} }
function renderComponentIfNode(node, ctx) { function renderComponentIfNode(node, ctx) {
let expression = "``"; let expression = "``";
const clientBranches = [];
for (let index = node.branches.length - 1; index >= 0; index--) { for (let index = node.branches.length - 1; index >= 0; index--) {
const branch = node.branches[index]; const branch = node.branches[index];
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join(""); const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
const bodyExpression = "`" + body + "`"; const bodyExpression = "`" + body + "`";
clientBranches.unshift(`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`);
expression = expression =
branch.cond === null branch.cond === null
? bodyExpression ? bodyExpression
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`; : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
} }
const definition = encodeClientControl(node.branches.map((branch) => ({ // Render all branches on the server into an inert, encoded definition.
cond: branch.cond, // A branch first selected in the browser therefore contains the real nested
body: renderClientControlTemplate(branch.body), // 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>`; return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
} }
function renderComponentEachNode(node, ctx) { function renderComponentEachNode(node, ctx) {
@@ -2901,7 +2985,7 @@ function renderComponentNode(node, ctx) {
} }
} }
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component"); 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:")) .filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
.map((a) => { .map((a) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name); const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
@@ -2964,6 +3048,18 @@ function renderComponentNode(node, ctx) {
const compiledValue = isExplicitComponentMount && wholeExpression const compiledValue = isExplicitComponentMount && wholeExpression
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}` ? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
: compileAttrValue(a.value, elementContext); : compileAttrValue(a.value, elementContext);
/*
* A constraint attribute whose value is only known at render time (a
* component prop defaulting to "") must be decided at render time, so
* the whole attribute -- name included -- is emitted by the helper.
* Component mounts are excluded: there the value is a prop being passed
* down, not an HTML attribute being written.
*/
if (isOmitWhenEmptyAttribute(a.name) && !isExplicitComponentMount) {
if (a.value === "")
return "";
return `\${__wrnOptionalAttr(${JSON.stringify(a.name)}, \`${compiledValue}\`)}`;
}
const rendered = ` ${a.name}="${compiledValue}"`; const rendered = ` ${a.name}="${compiledValue}"`;
const referencesState = exprRefsComponentReactiveValue(a.value, ctx); const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
const referencesLoopVariable = elementContext.loopVars const referencesLoopVariable = elementContext.loopVars
@@ -3264,6 +3360,10 @@ function __wrnHtml(v: unknown): string {
); );
} }
function __wrnOptionalAttr(name: string, value: string): string {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v: unknown): string { function __wrnAttr(v: unknown): string {
return String(v == null ? "" : v).replace( return String(v == null ? "" : v).replace(
/[&<>"]/g, /[&<>"]/g,
@@ -3343,6 +3443,9 @@ function __wrnRaw(v: unknown): string {
}`); }`);
} }
if (needsScope) { 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 { out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) { if (value === undefined) {
return "undefined"; return "undefined";
@@ -3415,6 +3518,9 @@ function wholeAttributeExpression(value) {
function __wrnHtml(v) { function __wrnHtml(v) {
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;"); return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;");
} }
function __wrnOptionalAttr(name, value) {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v) { function __wrnAttr(v) {
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;"); return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;");
} }
@@ -3422,7 +3528,7 @@ function __wrnProp(v) {
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v); const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
return __wrnAttr(value); return __wrnAttr(value);
} }
function renderPageComponentAttr(attr, dynamicExpressions) { function renderPageComponentAttr(attr, dynamicExpressions, _reactive) {
if (attr.event) { if (attr.event) {
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`; return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
} }
@@ -3435,7 +3541,11 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
} }
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; 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}`;
} }
}, },
@@ -3957,6 +4067,7 @@ function islandNamesFrom(imports) {
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.NativeCompileError = void 0; exports.NativeCompileError = void 0;
exports.generateNative = generateNative; exports.generateNative = generateNative;
const syntax_1 = require("@wrnexus/syntax");
class NativeCompileError extends Error { class NativeCompileError extends Error {
constructor(message) { constructor(message) {
super(message); super(message);
@@ -4022,12 +4133,12 @@ function expression(value) {
function textJsx(value) { function textJsx(value) {
const pieces = []; const pieces = [];
let last = 0; let last = 0;
for (const match of value.matchAll(/\{([^{}]+)\}/g)) { for (const segment of (0, syntax_1.extractInterpolations)(value)) {
if (match.index > last) if (segment.start > last)
pieces.push(value.slice(last, match.index)); pieces.push(value.slice(last, segment.start));
const expr = match[1].trim(); const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`); 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)); pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;")); return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
@@ -6117,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 = {}) { function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4); const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100; const printWidth = options.printWidth ?? 100;
@@ -6172,6 +6303,12 @@ function formatWrnPass(source, options = {}) {
index += 1; index += 1;
continue; continue;
} }
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value); const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value); const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value); const continuesControlBlock = isControlBlockMiddle(value);
@@ -6252,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); for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
}; };
Object.defineProperty(exports, "__esModule", { value: true }); 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"); var tokenizer_ts_1 = require("./tokenizer.js");
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } }); 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; } }); Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
@@ -6264,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, "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, "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; } }); 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"); var types_ts_1 = require("./types.js");
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return types_ts_1.eraseFunctionTypes; } }); 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; } }); Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return types_ts_1.inferredRuntimeType; } });
@@ -6297,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, "parseStructuredImports", { enumerable: true, get: function () { return v060_ts_1.parseStructuredImports; } });
Object.defineProperty(exports, "stripRuntimeFunctionModifiers", { enumerable: true, get: function () { return v060_ts_1.stripRuntimeFunctionModifiers; } }); 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) { "packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
"use strict"; "use strict";
@@ -6484,6 +6667,25 @@ function parse(source) {
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`); throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
} }
switch (kw.value) { switch (kw.value) {
case "auth": {
lx.next();
expect("eq");
const value = lx.next();
if (value.type !== "ident" || !["true", "false"].includes(value.value)) {
throw new ParseError(`Expected auth = true or false at offset ${value.pos}`);
}
security.auth = value.value === "true" ? "required" : "false";
break;
}
case "permission": {
lx.next();
expect("eq");
const value = expect("string").value.trim();
if (!value)
throw new ParseError(`Page permission cannot be empty at offset ${kw.pos}`);
security.permission = value;
break;
}
case "layout": { case "layout": {
// layout = "public" — selects app/layouts/<name>.wrn for this page. // layout = "public" — selects app/layouts/<name>.wrn for this page.
lx.next(); lx.next();
@@ -6762,7 +6964,7 @@ function parse(source) {
break; break;
} }
if (lx.peek().type === "lbrace") { if (lx.peek().type === "lbrace") {
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`); throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`);
} }
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`); throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
} }
@@ -7433,6 +7635,88 @@ exports.isIdentPart = isIdentPart;
* reimplementing it a second hand-rolled scanner is how apostrophes in * reimplementing it a second hand-rolled scanner is how apostrophes in
* prose used to swallow braces. * prose used to swallow braces.
*/ */
/**
* Identifiers that can precede a `/` without ending an expression, so the `/`
* opens a regex rather than dividing.
*/
const REGEX_PRECEDING_KEYWORDS = new Set([
"return",
"typeof",
"instanceof",
"in",
"of",
"new",
"delete",
"void",
"do",
"else",
"yield",
"await",
"case",
]);
/**
* Decide whether the `/` at `i` opens a regex literal or is a division sign.
*
* Scans backwards for the last significant character. `a / b` divides; `(/a/)`,
* `= /a/` and `return /a/` do not. Erring towards division is the safe
* direction -- mistaking division for a regex would swallow everything to the
* next `/` and lose any braces in between.
*/
function opensRegex(src, i) {
let j = i - 1;
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === "\n"))
j--;
if (j < 0)
return true;
const prev = src[j];
if (/[A-Za-z0-9_$]/.test(prev)) {
// An identifier ends an expression, so `/` divides -- unless it is a
// keyword that cannot end one, like `return`.
let k = j;
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
k--;
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
}
// `)` and `]` close an expression, `.` continues one, and a quote ends a
// literal; anything else leaves us in a position where a regex may start.
return (prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`");
}
/**
* Scan a regex literal starting at `i`, returning the index just past its
* closing `/` and flags, or null when this is not in fact a regex.
*
* A regex literal cannot span a newline, so an unterminated one is treated as
* "not a regex" rather than swallowing the rest of the file. That is what keeps
* a bare URL in view text (`https://example.com/a//b`) intact.
*/
function skipRegex(src, i) {
let j = i + 1;
let inClass = false;
while (j < src.length) {
const c = src[j];
if (c === "\n")
return null;
if (c === "\\") {
j += 2;
continue;
}
if (inClass) {
if (c === "]")
inClass = false;
}
else if (c === "[") {
inClass = true;
}
else if (c === "/") {
j++;
while (j < src.length && /[a-z]/.test(src[j]))
j++;
return j;
}
j++;
}
return null;
}
function skipLiteralOrComment(src, i, atLineStart) { function skipLiteralOrComment(src, i, atLineStart) {
const c = src[i]; const c = src[i];
if (c === "/" && src[i + 1] === "*") { if (c === "/" && src[i + 1] === "*") {
@@ -7456,6 +7740,13 @@ function skipLiteralOrComment(src, i, atLineStart) {
} }
return src.length; return src.length;
} }
// A regex literal is neither a string nor a brace pair, but it can contain
// both. Without this, a quote inside one opened a phantom string that
// swallowed every brace to the next quote, and a lone `{`/`}` miscounted
// block depth.
if (c === "/" && opensRegex(src, i)) {
return skipRegex(src, i);
}
return null; return null;
} }
class Lexer { class Lexer {
@@ -7464,8 +7755,13 @@ class Lexer {
constructor(src) { constructor(src) {
this.src = src; this.src = src;
} }
/** Skip whitespace and `// line comments`. */ /**
skipTrivia() { * Skip whitespace and `// line comments`, but NOT block comments.
*
* Kept separate from `skipTrivia` so `startsWithBlockComment` can still see a
* block comment that `skipTrivia` would otherwise consume.
*/
skipWhitespaceAndLineComments() {
const { src } = this; const { src } = this;
while (this.pos < src.length) { while (this.pos < src.length) {
const c = src[this.pos]; const c = src[this.pos];
@@ -7481,9 +7777,34 @@ class Lexer {
break; break;
} }
} }
/** True when the next non-trivia characters open a block comment. */ /**
* Skip whitespace, line comments and block comments.
*
* Block comments used to be skipped only inside a braced body, so one written
* between two members failed with a bare "Unexpected character '/'" -- the
* same comment parsed or did not depending on where it sat.
*/
skipTrivia() {
const { src } = this;
while (this.pos < src.length) {
this.skipWhitespaceAndLineComments();
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
const close = src.indexOf("*/", this.pos + 2);
this.pos = close === -1 ? src.length : close + 2;
continue;
}
break;
}
}
/**
* True when the next non-trivia characters open a block comment.
*
* Deliberately skips only whitespace and line comments: `props {}` refuses
* block comments with an explained error, and that check must run before
* `skipTrivia` would swallow the comment and drop a declaration silently.
*/
startsWithBlockComment() { startsWithBlockComment() {
this.skipTrivia(); this.skipWhitespaceAndLineComments();
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*"; return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
} }
/** Read and consume the next structural token. */ /** Read and consume the next structural token. */
File diff suppressed because it is too large Load Diff
+277 -12
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
// WRN editor language server source hash: 70d2454817cc3aa546304c880e0a82313bb73b50051556bf7febd12e6a46c78c // WRN editor language server source hash: 5258b3e76cec47df752d4bb886fbd57a3f3c4af9ca9b6b2bf6742c8d78025ff1
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72 // WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
// @bun @bun-cjs // @bun @bun-cjs
(function(exports, require, module, __filename, __dirname) {var __create = Object.create; (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)) : {}; target = mod != null ? __create(__getProtoOf(mod)) : {};
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; 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)) for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key)) if (!__hasOwnProp.call(to, key))
__defProp(to, key, { __defProp(to, key, {
get: __accessProp.bind(mod, key), get: __accessProp.bind(mod, key),
enumerable: true enumerable: true
}); });
}
if (canCache) if (canCache)
cache.set(mod, to); cache.set(mod, to);
return to; return to;
@@ -66,7 +68,7 @@ var __export = (target, all) => {
}; };
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js // 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"; 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. 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 // 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 __defProp2 = Object.defineProperty;
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor; var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
var __getOwnPropNames2 = Object.getOwnPropertyNames; var __getOwnPropNames2 = Object.getOwnPropertyNames;
@@ -169658,8 +169660,9 @@ var require_main = __commonJS((exports2, module2) => {
// packages/language-server/src/server.ts // packages/language-server/src/server.ts
var exports_server = {}; var exports_server = {};
__export(exports_server, { __export(exports_server, {
apiCallCompletions: () => apiCallCompletions,
apiCallHover: () => apiCallHover, apiCallHover: () => apiCallHover,
apiCallCompletions: () => apiCallCompletions isApiReferenceAt: () => isApiReferenceAt
}); });
module.exports = __toCommonJS(exports_server); module.exports = __toCommonJS(exports_server);
@@ -169670,6 +169673,64 @@ var isWs = (c) => c === " " || c === "\t" || c === `
` || c === "\r"; ` || c === "\r";
var isIdentStart = (c) => /[A-Za-z_]/.test(c); var isIdentStart = (c) => /[A-Za-z_]/.test(c);
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c); var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
var REGEX_PRECEDING_KEYWORDS = new Set([
"return",
"typeof",
"instanceof",
"in",
"of",
"new",
"delete",
"void",
"do",
"else",
"yield",
"await",
"case"
]);
function opensRegex(src, i) {
let j = i - 1;
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === `
`))
j--;
if (j < 0)
return true;
const prev = src[j];
if (/[A-Za-z0-9_$]/.test(prev)) {
let k = j;
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
k--;
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
}
return prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`";
}
function skipRegex(src, i) {
let j = i + 1;
let inClass = false;
while (j < src.length) {
const c = src[j];
if (c === `
`)
return null;
if (c === "\\") {
j += 2;
continue;
}
if (inClass) {
if (c === "]")
inClass = false;
} else if (c === "[") {
inClass = true;
} else if (c === "/") {
j++;
while (j < src.length && /[a-z]/.test(src[j]))
j++;
return j;
}
j++;
}
return null;
}
function skipLiteralOrComment(src, i, atLineStart) { function skipLiteralOrComment(src, i, atLineStart) {
const c = src[i]; const c = src[i];
if (c === "/" && src[i + 1] === "*") { if (c === "/" && src[i + 1] === "*") {
@@ -169694,6 +169755,9 @@ function skipLiteralOrComment(src, i, atLineStart) {
} }
return src.length; return src.length;
} }
if (c === "/" && opensRegex(src, i)) {
return skipRegex(src, i);
}
return null; return null;
} }
@@ -169703,7 +169767,7 @@ class Lexer {
constructor(src) { constructor(src) {
this.src = src; this.src = src;
} }
skipTrivia() { skipWhitespaceAndLineComments() {
const { src } = this; const { src } = this;
while (this.pos < src.length) { while (this.pos < src.length) {
const c = src[this.pos]; const c = src[this.pos];
@@ -169720,8 +169784,20 @@ class Lexer {
break; break;
} }
} }
skipTrivia() {
const { src } = this;
while (this.pos < src.length) {
this.skipWhitespaceAndLineComments();
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
const close = src.indexOf("*/", this.pos + 2);
this.pos = close === -1 ? src.length : close + 2;
continue;
}
break;
}
}
startsWithBlockComment() { startsWithBlockComment() {
this.skipTrivia(); this.skipWhitespaceAndLineComments();
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*"; return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
} }
next() { next() {
@@ -170646,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 = {}) { function formatWrnPass(source, options = {}) {
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4); const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
const printWidth = options.printWidth ?? 100; const printWidth = options.printWidth ?? 100;
@@ -170702,6 +170798,12 @@ function formatWrnPass(source, options = {}) {
index += 1; index += 1;
continue; continue;
} }
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
if (structuredProp) {
output.push(...structuredProp);
index += 1;
continue;
}
const leadingClosingBraces = countLeadingClosingBraces(value); const leadingClosingBraces = countLeadingClosingBraces(value);
const closesControlBlock = isControlBlockClose(value); const closesControlBlock = isControlBlockClose(value);
const continuesControlBlock = isControlBlockMiddle(value); const continuesControlBlock = isControlBlockMiddle(value);
@@ -171537,6 +171639,25 @@ function parse(source) {
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`); throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
} }
switch (kw.value) { switch (kw.value) {
case "auth": {
lx.next();
expect("eq");
const value = lx.next();
if (value.type !== "ident" || !["true", "false"].includes(value.value)) {
throw new ParseError(`Expected auth = true or false at offset ${value.pos}`);
}
security.auth = value.value === "true" ? "required" : "false";
break;
}
case "permission": {
lx.next();
expect("eq");
const value = expect("string").value.trim();
if (!value)
throw new ParseError(`Page permission cannot be empty at offset ${kw.pos}`);
security.permission = value;
break;
}
case "layout": { case "layout": {
lx.next(); lx.next();
expect("eq"); expect("eq");
@@ -171799,7 +171920,7 @@ function parse(source) {
break; break;
} }
if (lx.peek().type === "lbrace") { if (lx.peek().type === "lbrace") {
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`); throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`);
} }
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`); throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
} }
@@ -172298,6 +172419,44 @@ function parseHtmlView(src, pos) {
const nodes = parseNodeList("root"); const nodes = parseNodeList("root");
return { nodes, endPos: i }; 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 // packages/syntax/src/diagnostics.ts
function stripAsciiControlAndSpace(value) { function stripAsciiControlAndSpace(value) {
let result = ""; let result = "";
@@ -172895,7 +173054,7 @@ function runtimeNamespace(runtime) {
return `__wrn_${runtime}`; return `__wrn_${runtime}`;
} }
function functionDeclaration(fn) { 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}}`; return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
} }
function retainedImports(ast) { function retainedImports(ast) {
@@ -172955,6 +173114,27 @@ var RESERVED_BINDING_NAMES = new Set([
function safeBindingName(name) { function safeBindingName(name) {
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(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)) { function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
const ast = parse(source); const ast = parse(source);
const chunks = []; const chunks = [];
@@ -172995,14 +173175,23 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
const outputType = ast.outputs.map((output) => `${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`).join("; "); const outputType = ast.outputs.map((output) => `${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`).join("; ");
const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server"); const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server");
const serverType = serverFunctions.map((fn) => `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`).join("; "); const serverType = serverFunctions.map((fn) => `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`).join("; ");
const apiType = ast.dataApis.map((block) => {
const fields = block.sections ? [...block.sections.parameters, ...block.sections.body] : [];
const inputType = fields.length ? `{ ${fields.map((f) => `${f.name}${f.optional ? "?" : ""}: ${f.type}`).join("; ")} }` : "Record<string, never>";
return `${block.name}: (input${fields.length ? "" : "?"}: ${inputType}) => Promise<any>`;
}).join("; ");
append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`); append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
for (const output of ast.outputs) for (const output of ast.outputs)
append(`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`); append(`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`);
for (const match of source.matchAll(/@event\s+([A-Za-z_$][\w$]*)\s*=\s*function/g)) for (const match of source.matchAll(/@event\s+([A-Za-z_$][\w$]*)\s*=\s*function/g))
append(`declare const ${match[1]}: ((...args: unknown[]) => void) | undefined;`); append(`declare const ${match[1]}: ((...args: unknown[]) => void) | undefined;`);
append(`declare const server: { ${serverType} };`); append(`declare const server: { ${serverType} };`);
if (ast.dataApis.length)
append(`declare const api: { ${apiType} };`);
append(`declare const props: Readonly<${ast.name}Props>;`); append(`declare const props: Readonly<${ast.name}Props>;`);
append("declare const refs: Record<string, Element | null>;"); 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(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
append(importedWrnDeclarations(ast, filePath, appRoot)); append(importedWrnDeclarations(ast, filePath, appRoot));
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name)); const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
@@ -173019,6 +173208,53 @@ ${sharedAliases}`);
append(functionDeclaration(fn), fn.source); append(functionDeclaration(fn), fn.source);
append("}"); 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 { return {
ast, ast,
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"), fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
@@ -173124,7 +173360,7 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
const shape = shapes.get(node.tag); const shape = shapes.get(node.tag);
if (!shape) if (!shape)
return; 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 outputNames = new Set(shape.outputs.map((output) => output.name));
const position = lineAt(source, `<${node.tag}`); const position = lineAt(source, `<${node.tag}`);
for (const prop of shape.props) { for (const prop of shape.props) {
@@ -173161,7 +173397,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
} }
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs") if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
continue; 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) { if (!prop) {
diagnostics.push({ diagnostics.push({
code: "WRN-COMPONENT-UNKNOWN-PROP", code: "WRN-COMPONENT-UNKNOWN-PROP",
@@ -196009,6 +196246,9 @@ function getLanguageService(options = defaultLanguageServiceOptions) {
findLinkedEditingRanges findLinkedEditingRanges
}; };
} }
function newHTMLDataProvider(id, customData) {
return new HTMLDataProvider(id, customData);
}
// packages/language-server/src/html-regions.ts // packages/language-server/src/html-regions.ts
function viewRegions(text) { function viewRegions(text) {
@@ -196098,7 +196338,13 @@ var semanticTokensLegend2 = {
}; };
// packages/language-server/src/html-service.ts // packages/language-server/src/html-service.ts
var service = getLanguageService(); var wrnexusDataProvider = newHTMLDataProvider("wrnexus", {
version: 1,
globalAttributes: [
{ name: "api", description: "Binds this element to a declared `apis { }` entry." }
]
});
var service = getLanguageService({ customDataProviders: [wrnexusDataProvider] });
function htmlDocument(document) { function htmlDocument(document) {
const virtual = virtualHtmlDocument(document); const virtual = virtualHtmlDocument(document);
return TextDocument2.create(virtual.uri, "html", document.version ?? 1, virtual.text); return TextDocument2.create(virtual.uri, "html", document.version ?? 1, virtual.text);
@@ -196111,9 +196357,18 @@ function markdown(value) {
} }
return; return;
} }
function isApiAttributeValuePosition(document, position) {
const offset = offsetAt2(document.text, position);
if (!isInsideHtml(document, offset))
return false;
const before = document.text.slice(0, offset);
return /\bapi\s*=\s*(["'])(?:(?!\1)[^\n])*$/.test(before);
}
function htmlCompletions(document, position) { function htmlCompletions(document, position) {
if (!isInsideHtml(document, offsetAt2(document.text, position))) if (!isInsideHtml(document, offsetAt2(document.text, position)))
return []; return [];
if (isApiAttributeValuePosition(document, position))
return [];
const virtual = htmlDocument(document); const virtual = htmlDocument(document);
const parsed = service.parseHTMLDocument(virtual); const parsed = service.parseHTMLDocument(virtual);
const list = service.doComplete(virtual, position, parsed); const list = service.doComplete(virtual, position, parsed);
@@ -196243,6 +196498,12 @@ function isApiCallPosition(text, offset) {
const before = text.slice(0, offset); const before = text.slice(0, offset);
return /\bapi\.\w*$/.test(before); return /\bapi\.\w*$/.test(before);
} }
function isApiReferenceAt(text, offset) {
let start = offset;
while (start > 0 && /\w/.test(text[start - 1]))
start--;
return /\bapi\.$/.test(text.slice(0, start));
}
var documents = new Map; var documents = new Map;
var diagnosticTimers = new Map; var diagnosticTimers = new Map;
var MAX_OPEN_DOCUMENTS = 256; var MAX_OPEN_DOCUMENTS = 256;
@@ -196419,6 +196680,10 @@ async function handle(message) {
result(message.id, apiCallCompletions(document.text)); result(message.id, apiCallCompletions(document.text));
break; break;
} }
if (document && isApiAttributeValuePosition(document, params.position)) {
result(message.id, apiCallCompletions(document.text));
break;
}
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)]; const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
const html = document ? htmlCompletions(document, params.position) : []; const html = document ? htmlCompletions(document, params.position) : [];
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus); result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);
@@ -196437,7 +196702,7 @@ async function handle(message) {
case "textDocument/hover": { case "textDocument/hover": {
const document = documents.get(params.textDocument.uri); const document = documents.get(params.textDocument.uri);
const word = document ? wordAt(document.text, params.position)?.word : undefined; const word = document ? wordAt(document.text, params.position)?.word : undefined;
const apiHover = word ? apiCallHover(document.text, word) : undefined; const apiHover = word && isApiReferenceAt(document.text, offsetAt(document.text, params.position)) ? apiCallHover(document.text, word) : undefined;
if (apiHover) { if (apiHover) {
result(message.id, { contents: apiHover }); result(message.id, { contents: apiHover });
break; break;
+14
View File
@@ -20,6 +20,20 @@ test("preserves nested prop defaults while formatting", () => {
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted); 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", () => { test("formats native JSON state values with readable indentation", () => {
const source = `component Footer { const source = `component Footer {
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}] 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 { export interface WelcomeEmailPayload {
userId: number; userId: number;
email: string; 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", name: "welcome-email",
async run(job) { jobs: {
console.log(`Welcome email queued for ${job.data.email}`); 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", "generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
"sbom": "node scripts/generate-sbom.mjs", "sbom": "node scripts/generate-sbom.mjs",
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write", "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", "validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
"stage:packages": "bun run scripts/publish-packages.ts", "stage:packages": "bun run scripts/publish-packages.ts",
"test:staged-consumers": "node scripts/test-staged-consumers.mjs", "test:staged-consumers": "node scripts/test-staged-consumers.mjs",
@@ -70,7 +70,7 @@
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
"@types/bun": "^1.3.14", "@types/bun": "^1.4.0",
"@types/react": "^19.2.18", "@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.2.4",
"eslint": "^10.8.1", "eslint": "^10.8.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.13", "version": "0.8.18",
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.", "description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
+33 -1
View File
@@ -11,6 +11,7 @@ import {
import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts"; import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts";
import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts"; import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts";
import { evaluateAuthRisk } from "./risk.ts"; import { evaluateAuthRisk } from "./risk.ts";
import { setDefaultAuthEngine } from "./runtime.ts";
import type { AuthStore } from "./store.ts"; import type { AuthStore } from "./store.ts";
import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts"; import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts";
import type { import type {
@@ -266,7 +267,29 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
if (options.secret.length < MIN_SECRET_LENGTH) { if (options.secret.length < MIN_SECRET_LENGTH) {
throw new TypeError(`auth secret must be at least ${MIN_SECRET_LENGTH} characters`); throw new TypeError(`auth secret must be at least ${MIN_SECRET_LENGTH} characters`);
} }
const store = options.store; let resolvedStore: AuthStore | undefined;
const resolveStore = (): AuthStore => {
if (resolvedStore) return resolvedStore;
resolvedStore = typeof options.store === "function" ? options.store() : options.store;
if (!resolvedStore || typeof resolvedStore !== "object") {
throw new TypeError("WRN-AUTH-STORE: the auth store factory did not return an AuthStore");
}
return resolvedStore;
};
// AuthEngine.store remains source-compatible while deferring the factory
// until the first actual property read or method call.
const store = new Proxy({} as AuthStore, {
get(_target, property) {
const value = Reflect.get(resolveStore() as object, property);
return typeof value === "function" ? value.bind(resolveStore()) : value;
},
set(_target, property, value) {
return Reflect.set(resolveStore() as object, property, value);
},
has(_target, property) {
return Reflect.has(resolveStore() as object, property);
},
});
const now = () => { const now = () => {
const value = options.clock?.now() ?? Date.now(); const value = options.clock?.now() ?? Date.now();
if (!Number.isFinite(value)) throw new Error("WRN-AUTH-CLOCK: clock returned an invalid time"); if (!Number.isFinite(value)) throw new Error("WRN-AUTH-CLOCK: clock returned an invalid time");
@@ -1649,6 +1672,11 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
(typeof metadata.deviceId === "string" ? metadata.deviceId : undefined), (typeof metadata.deviceId === "string" ? metadata.deviceId : undefined),
trusted: Boolean(rememberedDevice) || input.session?.trusted === true, trusted: Boolean(rememberedDevice) || input.session?.trusted === true,
fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined, fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined,
metadata: {
...(input.session?.metadata ?? {}),
mfaVerifiedAt: now(),
mfaMethod: input.method,
},
}); });
user.lastLoginAt = now(); user.lastLoginAt = now();
user.updatedAt = now(); user.updatedAt = now();
@@ -2276,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; return engine;
} }
+94 -2
View File
@@ -10,6 +10,8 @@ import {
} from "../middleware.ts"; } from "../middleware.ts";
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts"; import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
import type { AuthSessionVerificationHandler } from "../types.ts"; import type { AuthSessionVerificationHandler } from "../types.ts";
import { completeAuth, startAuth, type OAuthProvider } from "@wrnexus/oauth";
import { assignDefaultAuthzRoles } from "@wrnexus/authz/defaults";
function text(value: unknown): string { function text(value: unknown): string {
return typeof value === "string" ? value : value == null ? "" : String(value); return typeof value === "string" ? value : value == null ? "" : String(value);
@@ -50,6 +52,8 @@ export interface AuthHttpOptions {
schemas?: AuthSchemaOverrides | AuthSchemaSet; schemas?: AuthSchemaOverrides | AuthSchemaSet;
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
onSessionVerification?: AuthSessionVerificationHandler; onSessionVerification?: AuthSessionVerificationHandler;
oauth?: Record<string, OAuthProvider>;
oauthMfaPath?: string;
} }
export function createAuthHttpHandlers(options: AuthHttpOptions) { export function createAuthHttpHandlers(options: AuthHttpOptions) {
@@ -59,6 +63,17 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
const onSignedOut = engine.onSignedOut; const onSignedOut = engine.onSignedOut;
const onSuccessfulSignUp = engine.onSuccessfulSignUp; const onSuccessfulSignUp = engine.onSuccessfulSignUp;
function oauthProvider(ctx: Context): OAuthProvider | undefined {
return options.oauth?.[String(ctx.params.provider ?? "").toLowerCase()];
}
function oauthRedirectUri(ctx: Context, provider: OAuthProvider): string {
return new URL(
`/api/auth/oauth/${encodeURIComponent(provider.name)}/callback`,
options.baseUrl ?? ctx.url.origin,
).toString();
}
function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response { function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback; const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
return Response.redirect(new URL(path, ctx.url), 303); return Response.redirect(new URL(path, ctx.url), 303);
@@ -80,6 +95,71 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
} }
return { return {
async oauthProviders(ctx: Context): Promise<Response> {
return json({
ok: true,
providers: Object.values(options.oauth ?? {}).map((provider) => ({
id: provider.name,
name: provider.name,
label: `Continue with ${provider.name.charAt(0).toUpperCase()}${provider.name.slice(1)}`,
href: `/api/auth/oauth/${encodeURIComponent(provider.name)}?returnTo=${encodeURIComponent(
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ??
"/",
)}`,
})),
});
},
async startOAuth(ctx: Context): Promise<Response> {
const provider = oauthProvider(ctx);
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
const returnTo =
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ?? "/";
const started = await startAuth(provider, { redirectUri: oauthRedirectUri(ctx, provider) });
ctx.cookies.transaction(`wrnexus.oauth.${provider.name}`).set({
state: started.state,
verifier: started.verifier,
returnTo,
});
return Response.redirect(started.url, 302);
},
async completeOAuth(ctx: Context): Promise<Response> {
const provider = oauthProvider(ctx);
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
const transaction = ctx.cookies
.transaction<{
state: string;
verifier: string;
returnTo: string;
}>(`wrnexus.oauth.${provider.name}`)
.consume();
const state = ctx.url.searchParams.get("state");
const code = ctx.url.searchParams.get("code");
if (!transaction || !state || transaction.state !== state || !code) {
return json({ ok: false, error: "OAuth transaction is invalid or expired" }, 400);
}
const completed = await completeAuth(provider, {
code,
verifier: transaction.verifier,
redirectUri: oauthRedirectUri(ctx, provider),
});
const result = await engine.loginWithOAuth(
provider.name,
completed.profile,
completed.tokens,
);
if (result.code === "mfa-required" && result.mfaToken) {
ctx.cookies.transaction("wrnexus.auth.mfa", { sameSite: "Lax", maxAge: 300 }).set({
mfaToken: result.mfaToken,
returnTo: transaction.returnTo,
});
return Response.redirect(new URL(options.oauthMfaPath ?? "/two-factor", ctx.url), 303);
}
if (!result.ok || !result.session || !result.user) return json(result, 401);
establishAuthSession(ctx, result.session, result.user);
if (onSignedIn) return onSignedIn(ctx, transaction.returnTo);
return Response.redirect(new URL(transaction.returnTo, ctx.url), 303);
},
async verifySession(ctx: Context): Promise<Response> { async verifySession(ctx: Context): Promise<Response> {
const user = getAuthUser(ctx); const user = getAuthUser(ctx);
if (options.onSessionVerification) { if (options.onSessionVerification) {
@@ -110,6 +190,8 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
}); });
if (!result.ok || !result.user) return json(result, 400); if (!result.ok || !result.user) return json(result, 400);
await assignDefaultAuthzRoles(result.user.id, "signup");
const action = await onSuccessfulSignUp?.(ctx, result.user); const action = await onSuccessfulSignUp?.(ctx, result.user);
if (action instanceof Response) return action; if (action instanceof Response) return action;
if (action?.autoSignIn) { if (action?.autoSignIn) {
@@ -230,6 +312,7 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
password: text(input.password) || undefined, password: text(input.password) || undefined,
displayName: text(input.displayName) || undefined, displayName: text(input.displayName) || undefined,
}); });
if (result.ok && result.user) await assignDefaultAuthzRoles(result.user.id, "invitation");
return json(result, result.ok ? 200 : 400); return json(result, result.ok ? 200 : 400);
}, },
@@ -403,12 +486,18 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
const validation = await parseBody(schemas.mfaComplete, ctx.req); const validation = await parseBody(schemas.mfaComplete, ctx.req);
if (!validation.ok) return validation.response; if (!validation.ok) return validation.response;
const input = validation.value; const input = validation.value;
const continuation = ctx.cookies
.transaction<{ mfaToken: string; returnTo?: string }>("wrnexus.auth.mfa", {
sameSite: "Lax",
maxAge: 300,
})
.consume();
const methodValue = text(input.method); const methodValue = text(input.method);
const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue) const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp") ? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
: "totp"; : "totp";
const result = await engine.completeMfa({ const result = await engine.completeMfa({
mfaToken: text(input.mfaToken), mfaToken: text(input.mfaToken) || continuation?.mfaToken || "",
method, method,
code: text(input.code), code: text(input.code),
challengeId: text(input.challengeId) || undefined, challengeId: text(input.challengeId) || undefined,
@@ -421,7 +510,10 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
if (!result.ok || !result.session || !result.user) return json(result, 400); if (!result.ok || !result.session || !result.user) return json(result, 400);
establishAuthSession(ctx, result.session, result.user); establishAuthSession(ctx, result.session, result.user);
if (onSignedIn) { if (onSignedIn) {
return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin)); return onSignedIn(
ctx,
safeAuthReturnTo(text(input.returnTo) || continuation?.returnTo, ctx.url.origin),
);
} }
return json(result); return json(result);
}, },
+3
View File
@@ -9,6 +9,8 @@ export {
clearAuthSession, clearAuthSession,
getAuthUser, getAuthUser,
getAuthSession, getAuthSession,
requireAuthUser,
AuthRequiredError,
isAuthenticatedContext, isAuthenticatedContext,
AUTH_SESSION_KEY, AUTH_SESSION_KEY,
} from "./middleware.ts"; } from "./middleware.ts";
@@ -20,6 +22,7 @@ export {
export { export {
authPlugin, authPlugin,
authComponentsDir, authComponentsDir,
validateProductionAuthConfig,
type AuthConfig, type AuthConfig,
type AuthRoutesConfig, type AuthRoutesConfig,
type AuthPluginOptions, type AuthPluginOptions,
+17
View File
@@ -79,6 +79,23 @@ export function getAuthUser(ctx: Context): AuthPublicUser | null {
); );
} }
/** Return a typed authenticated user or fail closed for direct handler use. */
export function requireAuthUser(ctx: Context): AuthPublicUser {
const user = getAuthUser(ctx);
if (!user) throw new AuthRequiredError();
return user;
}
export class AuthRequiredError extends Error {
readonly code = "WRN-AUTH-REQUIRED";
readonly status = 401;
constructor() {
super("Authentication is required");
this.name = "AuthRequiredError";
}
}
export function getAuthSession(ctx: Context): AuthSession | null { export function getAuthSession(ctx: Context): AuthSession | null {
return (ctx.locals.authSession as AuthSession | undefined) ?? null; return (ctx.locals.authSession as AuthSession | undefined) ?? null;
} }
+69
View File
@@ -2,6 +2,13 @@ import { readFileSync } from "node:fs";
import { dirname, join } from "node:path"; import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { definePlugin, type PluginContext } from "@wrnexus/plugin"; import { definePlugin, type PluginContext } from "@wrnexus/plugin";
import {
discord,
github,
google,
type OAuthProvider,
type ProviderCredentials,
} from "@wrnexus/oauth";
import type { AuthEngine } from "./engine.ts"; import type { AuthEngine } from "./engine.ts";
import type { AuthPasskeyHttpOptions } from "./http/index.ts"; import type { AuthPasskeyHttpOptions } from "./http/index.ts";
import type { AuthSessionVerificationHandler } from "./types.ts"; import type { AuthSessionVerificationHandler } from "./types.ts";
@@ -32,8 +39,11 @@ export interface AuthRoutesConfig {
sessions?: boolean; sessions?: boolean;
impersonation?: boolean; impersonation?: boolean;
passkeys?: boolean; passkeys?: boolean;
oauth?: boolean;
} }
export type AuthOAuthProviderConfig = OAuthProvider | ProviderCredentials;
export interface AuthConfig { export interface AuthConfig {
enabled?: boolean; enabled?: boolean;
engine?: AuthEngine; engine?: AuthEngine;
@@ -48,12 +58,36 @@ export interface AuthConfig {
baseUrl?: string; baseUrl?: string;
csrf?: boolean; csrf?: boolean;
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
oauth?: Partial<Record<"google" | "github" | "discord", AuthOAuthProviderConfig>> &
Record<string, AuthOAuthProviderConfig>;
oauthMfaPath?: string;
/** Disable only when an external deployment gate performs equivalent checks. */
productionValidation?: boolean;
/** Shared SSO cookie whose value is an AuthEngine session id. */ /** Shared SSO cookie whose value is an AuthEngine session id. */
sessionCookieName?: string; sessionCookieName?: string;
/** Customize the package forward-auth verification response. */ /** Customize the package forward-auth verification response. */
onSessionVerification?: AuthSessionVerificationHandler; onSessionVerification?: AuthSessionVerificationHandler;
} }
export function validateProductionAuthConfig(
config: Pick<AuthConfig, "baseUrl" | "oauth">,
env: Record<string, string | undefined> = process.env,
): void {
const secret = env.AUTH_SECRET?.trim() ?? "";
if (secret.length < 32 || /^(?:change-me|development|test-only)/i.test(secret)) {
throw new Error(
"WRN-AUTH-CONFIG: AUTH_SECRET must be a non-development secret of at least 32 characters in production",
);
}
if (!config.baseUrl) throw new Error("WRN-AUTH-CONFIG: auth.baseUrl is required in production");
const base = new URL(config.baseUrl);
if (base.protocol !== "https:")
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must use HTTPS in production");
if (["localhost", "127.0.0.1", "::1"].includes(base.hostname)) {
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must not use localhost in production");
}
}
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */ /** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
export interface AuthPluginOptions { export interface AuthPluginOptions {
componentDir?: string; componentDir?: string;
@@ -88,6 +122,8 @@ interface ResolvedAuthConfig {
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
sessionCookieName?: string; sessionCookieName?: string;
onSessionVerification?: AuthSessionVerificationHandler; onSessionVerification?: AuthSessionVerificationHandler;
oauth: Record<string, OAuthProvider>;
oauthMfaPath?: string;
} }
const moduleRoot = dirname(fileURLToPath(import.meta.url)); const moduleRoot = dirname(fileURLToPath(import.meta.url));
@@ -121,6 +157,24 @@ function resolveConfig(
const enabled = raw.enabled !== false; const enabled = raw.enabled !== false;
const hasEngine = Boolean(raw.engine); const hasEngine = Boolean(raw.engine);
const hasDefaultDb = Boolean(config.db); const hasDefaultDb = Boolean(config.db);
const oauth: Record<string, OAuthProvider> = {};
for (const [name, provider] of Object.entries(raw.oauth ?? {})) {
if (!provider || !provider.clientId?.trim() || !provider.clientSecret?.trim()) continue;
oauth[name] =
"authorizeUrl" in provider
? provider
: name === "google"
? google(provider)
: name === "github"
? github(provider)
: name === "discord"
? discord(provider)
: (() => {
throw new Error(
`WRN-AUTH-OAUTH-CONFIG: custom provider '${name}' requires a complete OAuthProvider`,
);
})();
}
return { return {
enabled, enabled,
@@ -148,6 +202,8 @@ function resolveConfig(
passkey: raw.passkey, passkey: raw.passkey,
sessionCookieName: raw.sessionCookieName, sessionCookieName: raw.sessionCookieName,
onSessionVerification: raw.onSessionVerification, onSessionVerification: raw.onSessionVerification,
oauth,
oauthMfaPath: raw.oauthMfaPath,
}; };
} }
@@ -176,6 +232,7 @@ function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
schemas: resolveAuthSchemas(), schemas: resolveAuthSchemas(),
csrf: true, csrf: true,
oauth: {},
}; };
} }
@@ -353,6 +410,16 @@ export function authPlugin(options: AuthPluginOptions = {}) {
}, },
configure(config, context) { configure(config, context) {
const raw = (config.auth ?? {}) as AuthConfig;
if (
context.mode === "production" &&
process.env.NODE_ENV === "production" &&
raw.enabled !== false &&
raw.engine &&
raw.productionValidation !== false
) {
validateProductionAuthConfig(raw);
}
const value = resolveConfig(config, options); const value = resolveConfig(config, options);
context.metadata.set(resolvedConfigKey, value); context.metadata.set(resolvedConfigKey, value);
@@ -383,6 +450,8 @@ export function authPlugin(options: AuthPluginOptions = {}) {
sessionCookieName: value.sessionCookieName, sessionCookieName: value.sessionCookieName,
onSessionVerification: value.onSessionVerification, onSessionVerification: value.onSessionVerification,
oauth: value.oauth,
oauthMfaPath: value.oauthMfaPath,
}); });
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir); context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
+2
View File
@@ -61,6 +61,8 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
baseUrl: routeOptions.baseUrl ?? ctx.url.origin, baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
passkey: routeOptions.passkey, passkey: routeOptions.passkey,
onSessionVerification: routeOptions.onSessionVerification, onSessionVerification: routeOptions.onSessionVerification,
oauth: routeOptions.oauth,
oauthMfaPath: routeOptions.oauthMfaPath,
}); });
} }
@@ -0,0 +1,6 @@
import type { Context } from "@wrnexus/core";
import { invokeAuthHandler } from "../api.ts";
export function GET(ctx: Context): Promise<Response> {
return invokeAuthHandler("completeOAuth", ctx);
}
@@ -0,0 +1,6 @@
import type { Context } from "@wrnexus/core";
import { invokeAuthHandler } from "../api.ts";
export function GET(ctx: Context): Promise<Response> {
return invokeAuthHandler("startOAuth", ctx);
}
@@ -0,0 +1,6 @@
import type { Context } from "@wrnexus/core";
import { invokeAuthHandler } from "../api.ts";
export function GET(ctx: Context): Promise<Response> {
return invokeAuthHandler("oauthProviders", ctx);
}
+20 -1
View File
@@ -12,7 +12,8 @@ export type AuthRouteGroup =
| "mfa" | "mfa"
| "sessions" | "sessions"
| "impersonation" | "impersonation"
| "passkeys"; | "passkeys"
| "oauth";
export interface AuthRouteDefinition { export interface AuthRouteDefinition {
path: string; path: string;
@@ -23,6 +24,24 @@ export interface AuthRouteDefinition {
/** Single source of truth for package-contributed auth endpoints. */ /** Single source of truth for package-contributed auth endpoints. */
export const AUTH_ROUTE_DEFINITIONS = [ export const AUTH_ROUTE_DEFINITIONS = [
{
path: "/api/auth/oauth/providers",
group: "oauth",
handler: "oauthProviders",
methods: ["GET"],
},
{
path: "/api/auth/oauth/[provider]",
group: "oauth",
handler: "startOAuth",
methods: ["GET"],
},
{
path: "/api/auth/oauth/[provider]/callback",
group: "oauth",
handler: "completeOAuth",
methods: ["GET"],
},
{ {
path: "/api/auth/session/verify", path: "/api/auth/session/verify",
group: "sessions", group: "sessions",
+3
View File
@@ -1,6 +1,7 @@
import type { AuthEngine } from "./engine.ts"; import type { AuthEngine } from "./engine.ts";
import type { AuthPasskeyHttpOptions } from "./http/index.ts"; import type { AuthPasskeyHttpOptions } from "./http/index.ts";
import type { AuthSessionVerificationHandler } from "./types.ts"; import type { AuthSessionVerificationHandler } from "./types.ts";
import type { OAuthProvider } from "@wrnexus/oauth";
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts"; import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
export interface DefaultAuthRouteOptions { export interface DefaultAuthRouteOptions {
@@ -9,6 +10,8 @@ export interface DefaultAuthRouteOptions {
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
sessionCookieName?: string; sessionCookieName?: string;
onSessionVerification?: AuthSessionVerificationHandler; onSessionVerification?: AuthSessionVerificationHandler;
oauth?: Record<string, OAuthProvider>;
oauthMfaPath?: string;
} }
interface AuthRuntimeState { interface AuthRuntimeState {
+7 -2
View File
@@ -403,7 +403,12 @@ export interface AuthRandom {
} }
export interface AuthEngineOptions { export interface AuthEngineOptions {
store: import("./store.ts").AuthStore; /**
* Authentication storage may be created lazily. This is the preferred form
* for database-backed stores because application configuration is imported
* before the runtime database connection is installed.
*/
store: import("./store.ts").AuthStore | (() => import("./store.ts").AuthStore);
secret: string; secret: string;
issuer?: string; issuer?: string;
delivery?: AuthDeliveryProvider; delivery?: AuthDeliveryProvider;
@@ -501,7 +506,7 @@ export interface AuthResult {
}; };
} }
export interface AuthenticatedContext extends Context { export interface AuthenticatedContext extends Context<Record<string, string>, AuthPublicUser> {
user: AuthPublicUser; user: AuthPublicUser;
locals: Context["locals"] & { locals: Context["locals"] & {
authUser: AuthPublicUser; authUser: AuthPublicUser;
+1 -5
View File
@@ -160,11 +160,7 @@ export const mfaOtpRequestSchema = v.object({
}); });
export const mfaSchema = v.object({ export const mfaSchema = v.object({
mfaToken: v mfaToken: v.string().min(20, "MFA transaction is invalid").max(512).optional(),
.string()
.required("MFA transaction is missing")
.min(20, "MFA transaction is invalid")
.max(512),
method: v method: v
.string() .string()
.required("Choose a verification method") .required("Choose a verification method")
+19
View File
@@ -4,6 +4,25 @@ import { MemoryAuthStore } from "../src/stores/memory.ts";
import { generateTotp } from "../src/totp/index.ts"; import { generateTotp } from "../src/totp/index.ts";
import type { AuthDeliveryMessage, PasskeyProvider } from "../src/types.ts"; import type { AuthDeliveryMessage, PasskeyProvider } from "../src/types.ts";
test("authentication storage factories resolve lazily and only once", async () => {
let calls = 0;
const backing = new MemoryAuthStore();
const engine = createAuthEngine({
store: () => {
calls++;
return backing;
},
secret: "a secure test secret that is longer than thirty-two characters",
});
expect(calls).toBe(0);
expect(engine.store).toBeDefined();
expect(calls).toBe(0);
await engine.getUser("missing");
await engine.getUser("still-missing");
expect(calls).toBe(1);
});
function fixture() { function fixture() {
let time = 1_720_000_000_000; let time = 1_720_000_000_000;
let seed = 11; let seed = 11;
+67
View File
@@ -0,0 +1,67 @@
import { afterEach, expect, test } from "bun:test";
import { createContext, withContextHeaders } from "@wrnexus/core";
import type { OAuthProvider } from "@wrnexus/oauth";
import { createAuthEngine } from "../src/engine.ts";
import { createAuthHttpHandlers } from "../src/http/index.ts";
import { MemoryAuthStore } from "../src/stores/memory.ts";
const realFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = realFetch;
});
test("configured OAuth owns PKCE state, callback, and session establishment", async () => {
const provider: OAuthProvider = {
name: "example",
authorizeUrl: "https://identity.example/authorize",
tokenUrl: "https://identity.example/token",
userInfoUrl: "https://identity.example/user",
scopes: ["openid", "email"],
clientId: "client",
clientSecret: "secret",
mapProfile: (raw) => ({
id: String(raw.id),
email: String(raw.email),
raw,
}),
};
const engine = createAuthEngine({
store: new MemoryAuthStore(),
secret: "oauth-http-secret-that-is-longer-than-thirty-two-characters",
});
const handlers = createAuthHttpHandlers({
engine,
baseUrl: "https://app.example",
oauth: { example: provider },
});
const startRequest = new Request(
"https://app.example/api/auth/oauth/example?returnTo=%2Fdashboard",
);
const startCtx = createContext(startRequest, new URL(startRequest.url));
startCtx.params = { provider: "example" };
const start = await handlers.startOAuth(startCtx);
expect(start.status).toBe(302);
const location = new URL(start.headers.get("location")!);
expect(location.searchParams.get("code_challenge_method")).toBe("S256");
const issued = withContextHeaders(startCtx, start).headers.get("set-cookie")!;
const replies = [
Response.json({ access_token: "access", token_type: "Bearer" }),
Response.json({ id: "provider-user", email: "oauth@example.test" }),
];
const mock: typeof fetch = Object.assign(async () => replies.shift()!, {
preconnect: () => undefined,
});
globalThis.fetch = mock;
const callbackRequest = new Request(
`https://app.example/api/auth/oauth/example/callback?code=code&state=${encodeURIComponent(location.searchParams.get("state")!)}`,
{ headers: { cookie: issued.split(";", 1)[0]! } },
);
const callbackCtx = createContext(callbackRequest, new URL(callbackRequest.url));
callbackCtx.params = { provider: "example" };
const callback = await handlers.completeOAuth(callbackCtx);
expect(callback.status).toBe(303);
expect(callback.headers.get("location")).toBe("https://app.example/dashboard");
expect(callbackCtx.user).toMatchObject({ id: expect.any(String) });
});
+16 -1
View File
@@ -1,7 +1,7 @@
import { expect, test } from "bun:test"; import { expect, test } from "bun:test";
import type { Context } from "@wrnexus/core"; import type { Context } from "@wrnexus/core";
import { createPluginRunner } from "@wrnexus/plugin"; import { createPluginRunner } from "@wrnexus/plugin";
import { authPlugin } from "../src/plugin.ts"; import { authPlugin, validateProductionAuthConfig } from "../src/plugin.ts";
import { AUTH_ROUTE_DEFINITIONS } from "../src/routes/definitions.ts"; import { AUTH_ROUTE_DEFINITIONS } from "../src/routes/definitions.ts";
import { readFileSync } from "node:fs"; import { readFileSync } from "node:fs";
import { createAuthEngine } from "../src/engine.ts"; import { createAuthEngine } from "../src/engine.ts";
@@ -65,6 +65,21 @@ test("plugin contributes components, runtime, styles, migration, and toolbar", a
expect(contributions.middleware).toHaveLength(1); expect(contributions.middleware).toHaveLength(1);
}); });
test("production auth validation rejects development secrets and origins", () => {
expect(() =>
validateProductionAuthConfig(
{ baseUrl: "http://localhost:3000" },
{ AUTH_SECRET: "change-me" },
),
).toThrow("AUTH_SECRET");
expect(() =>
validateProductionAuthConfig(
{ baseUrl: "http://app.example" },
{ AUTH_SECRET: "a-production-secret-that-is-longer-than-thirty-two-characters" },
),
).toThrow("HTTPS");
});
test("unconfigured automatic discovery fails closed for routes, middleware, and migrations", async () => { test("unconfigured automatic discovery fails closed for routes, middleware, and migrations", async () => {
const metadata = new Map<string, unknown>(); const metadata = new Map<string, unknown>();
const runner = createPluginRunner(authPlugin(), { const runner = createPluginRunner(authPlugin(), {
+15 -3
View File
@@ -1,15 +1,27 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.8.9", "version": "0.8.15",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./db": "./src/db.ts" "./db": "./src/db.ts",
"./plugin": "./src/plugin.ts",
"./defaults": "./src/defaults.ts",
"./runtime-middleware": "./src/runtime-middleware.ts",
"./runtime-memory-middleware": "./src/runtime-memory-middleware.ts"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*" "@wrnexus/db": "workspace:*",
"@wrnexus/plugin": "workspace:*"
},
"wrnexus": {
"plugin": {
"plugin": "./src/plugin.ts",
"export": "default",
"factory": true
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import type { DecisionPolicy } from "./advanced.ts";
export interface CatalogSource { export interface CatalogSource {
/** File or package that declared this module, used in conflict messages. */ /** File or package that declared this module, used in conflict messages. */
source: string; source: string;
module: AuthzModule; module: AuthzModule<never, never>;
} }
/** Structural equality for declaration metadata. Key order is irrelevant. */ /** Structural equality for declaration metadata. Key order is irrelevant. */
+13 -3
View File
@@ -32,16 +32,26 @@
import type { AuthzCatalog } from "./types.ts"; 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). */ /** Set the process-wide authorization catalog (called by the framework at boot). */
export function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog { export function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog {
catalog = next; state().catalog = next;
return next; return next;
} }
/** The process-wide authorization catalog. Throws if it hasn't been set. */ /** The process-wide authorization catalog. Throws if it hasn't been set. */
export function getAuthzCatalog(): AuthzCatalog { export function getAuthzCatalog(): AuthzCatalog {
const catalog = state().catalog;
if (!catalog) { if (!catalog) {
throw new Error( throw new Error(
"WRN-AUTHZ-SETUP: no authorization catalog is configured. The dev server and " + "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. */ /** Whether the process-wide authorization catalog has been set. */
export function hasAuthzCatalog(): boolean { export function hasAuthzCatalog(): boolean {
return catalog !== undefined; return state().catalog !== undefined;
} }
+40
View File
@@ -0,0 +1,40 @@
import { getDb } from "@wrnexus/db";
import { dbPermissionStore } from "./db.ts";
import type { PermissionStore } from "./store.ts";
export type AuthzIdentityEvent = "signup" | "invitation";
interface DefaultRoleState {
signup: string[];
invitation: string[];
store?: PermissionStore;
}
const key = Symbol.for("@wrnexus/authz:default-roles:v1");
function state(): DefaultRoleState {
const global = globalThis as Record<PropertyKey, unknown>;
return (global[key] ??= { signup: [], invitation: [] }) as DefaultRoleState;
}
export function setDefaultAuthzRoles(roles: Partial<DefaultRoleState>): void {
state().signup = [...(roles.signup ?? [])];
state().invitation = [...(roles.invitation ?? [])];
}
/** Bind lifecycle role assignment to the same store used by authorization middleware. */
export function setDefaultAuthzRoleStore(store: PermissionStore): void {
state().store = store;
}
/** Assign configured identity lifecycle roles through the framework-owned store. */
export async function assignDefaultAuthzRoles(
subjectId: string,
event: AuthzIdentityEvent,
): Promise<void> {
if (!subjectId) return;
const roles = state()[event];
if (!roles.length) return;
const store = state().store ?? dbPermissionStore(getDb());
await Promise.all(roles.map((role) => store.assignRole(subjectId, role)));
}
+4 -1
View File
@@ -157,8 +157,11 @@ export {
guardPermission, guardPermission,
filterCan, filterCan,
AUTHZ_LOCALS_KEY, AUTHZ_LOCALS_KEY,
getRequestAuthorization,
} from "./middleware.ts"; } from "./middleware.ts";
export type { GuardOptions } from "./middleware.ts"; export type { GuardOptions, AuthorizationResponses, RequestAuthorization } from "./middleware.ts";
export { defineAuthorizedHandler, defineOwnedResource } from "./resource.ts";
export type { AuthorizedHandlerOptions, OwnedResourceDefinition } from "./resource.ts";
export type { export type {
AuthzScope, AuthzScope,
AuthzCatalog, AuthzCatalog,
+52 -10
View File
@@ -10,6 +10,27 @@ import type { AuthzScope } from "./types.ts";
*/ */
export const AUTHZ_LOCALS_KEY = "_authz"; export const AUTHZ_LOCALS_KEY = "_authz";
export interface AuthorizationResponses {
forbidden(decision?: AuthorizationDecision, options?: { exposeReason?: boolean }): Response;
notFoundOrForbidden(options?: {
mayDiscover?: boolean;
decision?: AuthorizationDecision;
exposeReason?: boolean;
}): Response;
}
export interface RequestAuthorization extends AuthorizationResponses {
can(permission: string, resource?: unknown): Promise<boolean>;
decide(permission: string, resource?: unknown): Promise<AuthorizationDecision>;
}
declare module "@wrnexus/core" {
interface Context {
/** Installed by authzMiddleware for handlers that prefer context-local authorization. */
authz?: RequestAuthorization;
}
}
interface RequestAuthz { interface RequestAuthz {
resolver: AuthzResolver; resolver: AuthzResolver;
/** /**
@@ -49,10 +70,39 @@ export function authzMiddleware(options: AuthzResolverOptions): Middleware {
byValue: new Map(), byValue: new Map(),
}; };
ctx.locals[AUTHZ_LOCALS_KEY] = request; ctx.locals[AUTHZ_LOCALS_KEY] = request;
ctx.authz = {
can: (permission, resource) => can(ctx, permission, resource),
decide: (permission, resource) => decideFor(ctx, permission, resource),
forbidden: (decision, responseOptions) =>
forbiddenResponse(decision, responseOptions?.exposeReason),
notFoundOrForbidden: (responseOptions = {}) =>
responseOptions.mayDiscover
? Response.json(
{ ok: false, error: "Not Found" },
{ status: 404, headers: NO_STORE_HEADERS },
)
: forbiddenResponse(responseOptions.decision, responseOptions.exposeReason),
};
return next(); return next();
}; };
} }
function forbiddenResponse(decision?: AuthorizationDecision, exposeReason = false): Response {
return Response.json(
exposeReason
? { ok: false, error: "Forbidden", reason: decision?.reason, policy: decision?.policy }
: { ok: false, error: "Forbidden" },
{ status: 403, headers: NO_STORE_HEADERS },
);
}
export function getRequestAuthorization(ctx: Context): RequestAuthorization {
if (!ctx.authz) {
throw new Error("WRN-AUTHZ-SETUP: authzMiddleware() is not registered for this request.");
}
return ctx.authz;
}
/** /**
* Read the tenant from the context at decision time, not at middleware time: * Read the tenant from the context at decision time, not at middleware time:
* a request that switches tenant mid-flight must not keep the old scope. * a request that switches tenant mid-flight must not keep the old scope.
@@ -236,10 +286,7 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
reason: "Resource unavailable", reason: "Resource unavailable",
at: Date.now(), at: Date.now(),
}); });
return Response.json( return forbiddenResponse();
{ ok: false, error: "Forbidden" },
{ status: 403, headers: NO_STORE_HEADERS },
);
} }
} }
@@ -264,12 +311,7 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
); );
} }
return Response.json( return forbiddenResponse(result, options.exposeReason);
options.exposeReason
? { ok: false, error: "Forbidden", reason: result.reason, policy: result.policy }
: { ok: false, error: "Forbidden" },
{ status: 403, headers: NO_STORE_HEADERS },
);
}; };
} }
+96
View File
@@ -0,0 +1,96 @@
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
import { authzMigrationSql } from "./migrations.ts";
import { setDefaultAuthzRoles } from "./defaults.ts";
export interface AuthzConfig {
enabled?: boolean;
declarations?: string;
store?: "database" | "memory";
migrations?: boolean;
middleware?: boolean;
strict?: boolean;
defaultRoles?: {
signup?: string[];
invitation?: string[];
};
}
interface ResolvedAuthzConfig {
enabled: boolean;
store: "database" | "memory";
migrations: boolean;
middleware: boolean;
strict: boolean;
defaultRoles: { signup: string[]; invitation: string[] };
driver: "sqlite" | "postgres" | "mysql";
}
const moduleRoot = dirname(fileURLToPath(import.meta.url));
const runtimeExtension = basename(moduleRoot) === "dist" ? ".js" : ".ts";
const middlewareEntry = join(moduleRoot, `runtime-middleware${runtimeExtension}`);
const memoryMiddlewareEntry = join(moduleRoot, `runtime-memory-middleware${runtimeExtension}`);
const metadataKey = "@wrnexus/authz:config";
function resolved(context: PluginContext): ResolvedAuthzConfig {
return (
(context.metadata.get(metadataKey) as ResolvedAuthzConfig | undefined) ?? {
enabled: false,
store: "database",
migrations: false,
middleware: false,
strict: true,
defaultRoles: { signup: [], invitation: [] },
driver: "sqlite",
}
);
}
export function authzPlugin() {
return definePlugin({
name: "@wrnexus/authz",
version: "0.8.0",
configure(config, context) {
const raw = (config.authz ?? {}) as AuthzConfig;
const enabled = raw.enabled !== false && Boolean(config.authz);
const db = config.db as { driver?: ResolvedAuthzConfig["driver"] } | undefined;
const value: ResolvedAuthzConfig = {
enabled,
store: raw.store ?? "database",
migrations: raw.migrations ?? enabled,
middleware: raw.middleware ?? enabled,
strict: raw.strict ?? true,
defaultRoles: {
signup: [...(raw.defaultRoles?.signup ?? [])],
invitation: [...(raw.defaultRoles?.invitation ?? [])],
},
driver: db?.driver ?? "sqlite",
};
if (enabled && value.store === "database" && !config.db) {
throw new Error("WRN-AUTHZ-CONFIG: authz.store='database' requires config.db");
}
context.metadata.set(metadataKey, value);
setDefaultAuthzRoles(value.defaultRoles);
config.authz = { ...raw, ...value };
},
middleware(context) {
const value = resolved(context);
if (!value.enabled || !value.middleware) return [];
return [value.store === "database" ? middlewareEntry : memoryMiddlewareEntry];
},
migrations(context) {
const value = resolved(context);
if (!value.enabled || !value.migrations || value.store !== "database") return [];
const sql = authzMigrationSql(value.driver);
return [
{
id: "wrnexus-authz-001",
source: `-- +up\n${sql.up.join(";\n")}\n;\n-- +down\n${sql.down.join(";\n")}\n;`,
},
];
},
});
}
export default authzPlugin;
+9 -2
View File
@@ -1,4 +1,4 @@
import type { AuthzModule } from "./types.ts"; import type { AuthzModule, DefinedAuthzModule } from "./types.ts";
const PERMISSION_ID = /^[a-z0-9]+(?::[a-z0-9-]+)+$/; const PERMISSION_ID = /^[a-z0-9]+(?::[a-z0-9-]+)+$/;
@@ -6,7 +6,9 @@ const PERMISSION_ID = /^[a-z0-9]+(?::[a-z0-9-]+)+$/;
* Validate and freeze one authorization declaration. Called from * Validate and freeze one authorization declaration. Called from
* `app/authz/<name>.ts` as the module's default export. * `app/authz/<name>.ts` as the module's default export.
*/ */
export function defineAuthz(module: AuthzModule): AuthzModule { export function defineAuthz<Subject = any, Resource = any>(
module: AuthzModule<Subject, Resource>,
): DefinedAuthzModule<Subject, Resource> {
const permissions = module.permissions ?? {}; const permissions = module.permissions ?? {};
const roles = module.roles ?? {}; const roles = module.roles ?? {};
const policies = module.policies ?? {}; const policies = module.policies ?? {};
@@ -14,6 +16,11 @@ export function defineAuthz(module: AuthzModule): AuthzModule {
const bindings = module.bindings ?? {}; const bindings = module.bindings ?? {};
for (const id of Object.keys(permissions)) { 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("*")) { if (id.includes("*")) {
throw new Error( throw new Error(
`WRN-AUTHZ-DECL: permission id '${id}' must not contain a wildcard; wildcards belong in roles.`, `WRN-AUTHZ-DECL: permission id '${id}' must not contain a wildcard; wildcards belong in roles.`,
+59
View File
@@ -0,0 +1,59 @@
import type { Context } from "@wrnexus/core";
import { decideFor } from "./middleware.ts";
export interface AuthorizedHandlerOptions<Resource, Result extends Response = Response> {
permission: string;
resource?: (ctx: Context) => Resource | null | undefined | Promise<Resource | null | undefined>;
exposeReason?: boolean;
mayDiscover?: (ctx: Context) => boolean | Promise<boolean>;
handle(ctx: Context & { resource: Resource }): Result | Promise<Result>;
}
/** Define an API handler whose resource loading and permission check cannot be skipped. */
export function defineAuthorizedHandler<Resource = undefined, Result extends Response = Response>(
options: AuthorizedHandlerOptions<Resource, Result>,
): (ctx: Context) => Promise<Response> {
return async (ctx) => {
if (!ctx.user) return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
const resource = options.resource ? await options.resource(ctx) : (undefined as Resource);
if (options.resource && resource == null) {
const mayDiscover = await options.mayDiscover?.(ctx);
return mayDiscover
? Response.json({ ok: false, error: "Not Found" }, { status: 404 })
: Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
}
const decision = await decideFor(ctx, options.permission, resource);
if (!decision.allowed) {
return ctx.authz!.forbidden(decision, { exposeReason: options.exposeReason });
}
ctx.resource = resource;
return options.handle(ctx as Context & { resource: Resource });
};
}
export interface OwnedResourceDefinition<Resource> {
name: string;
owner(resource: Resource): string | null | undefined;
permissions: {
read?: string;
create?: string;
update?: string;
delete?: string;
readAny?: string;
writeAny?: string;
};
}
/** Shared ownership and override vocabulary for application resource modules. */
export function defineOwnedResource<Resource>(definition: OwnedResourceDefinition<Resource>) {
return Object.freeze({
...definition,
attributes(resource: Resource) {
return { ownerId: definition.owner(resource) };
},
isOwner(subjectId: string, resource: Resource) {
const ownerId = definition.owner(resource);
return Boolean(subjectId && ownerId && subjectId === ownerId);
},
});
}
@@ -0,0 +1,12 @@
import { getAuthzCatalog } from "./client.ts";
import { setDefaultAuthzRoleStore } from "./defaults.ts";
import { authzMiddleware } from "./middleware.ts";
import { memoryPermissionStore } from "./store.ts";
/** Process-local authorization storage for development and stateless tests. */
const store = memoryPermissionStore();
setDefaultAuthzRoleStore(store);
export default function frameworkMemoryAuthz(ctx: Context, next: Next) {
return authzMiddleware({ catalog: getAuthzCatalog(), store, strict: true })(ctx, next);
}
import type { Context, Next } from "@wrnexus/core";
+31
View File
@@ -0,0 +1,31 @@
import type { Context, Next } from "@wrnexus/core";
import { getDb, type Db } from "@wrnexus/db";
import { getAuthzCatalog } from "./client.ts";
import { dbPermissionStore, ensureAuthzTables } from "./db.ts";
import { setDefaultAuthzRoleStore } from "./defaults.ts";
import { authzMiddleware } from "./middleware.ts";
const initialized = new WeakMap<Db, Promise<void>>();
function initialize(db: Db): Promise<void> {
let pending = initialized.get(db);
if (!pending) {
pending = ensureAuthzTables(db);
initialized.set(db, pending);
pending.catch(() => initialized.delete(db));
}
return pending;
}
/** Package-installed authorization middleware with lazy database resolution. */
export default async function frameworkAuthz(ctx: Context, next: Next): Promise<Response> {
const db = getDb();
await initialize(db);
const store = dbPermissionStore(db);
setDefaultAuthzRoleStore(store);
return authzMiddleware({
catalog: getAuthzCatalog(),
store,
strict: true,
})(ctx, next);
}
+11 -2
View File
@@ -18,15 +18,24 @@ export interface AttributeMeta {
} }
/** One `app/authz/<name>.ts` declaration. */ /** One `app/authz/<name>.ts` declaration. */
export interface AuthzModule { export interface AuthzModule<Subject = any, Resource = any> {
permissions?: Record<string, PermissionMeta>; permissions?: Record<string, PermissionMeta>;
roles?: Record<string, string[]>; roles?: Record<string, string[]>;
policies?: Record<string, DecisionPolicy<never, never>>; policies?: Record<string, DecisionPolicy<Subject, Resource>>;
attributes?: Record<string, AttributeMeta>; attributes?: Record<string, AttributeMeta>;
/** permission id -> policy names that must pass for it. */ /** permission id -> policy names that must pass for it. */
bindings?: Record<string, string[]>; bindings?: Record<string, string[]>;
} }
/** A declaration after defineAuthz() has supplied all optional collections. */
export type DefinedAuthzModule<Subject = any, Resource = any> = {
permissions: Record<string, PermissionMeta>;
roles: Record<string, string[]>;
policies: Record<string, DecisionPolicy<Subject, Resource>>;
attributes: Record<string, AttributeMeta>;
bindings: Record<string, string[]>;
};
/** The merged, frozen view of every declaration in the app. */ /** The merged, frozen view of every declaration in the app. */
export interface AuthzCatalog { export interface AuthzCatalog {
permissions: ReadonlyMap<string, PermissionMeta>; permissions: ReadonlyMap<string, PermissionMeta>;
+47
View File
@@ -0,0 +1,47 @@
import { expect, test } from "bun:test";
import { createPluginRunner } from "@wrnexus/plugin";
import { authzPlugin } from "../src/plugin.ts";
function runner() {
return createPluginRunner(authzPlugin(), {
root: process.cwd(),
mode: "development",
command: "dev",
metadata: new Map(),
warn() {},
});
}
test("authz plugin is inert until configured", async () => {
const instance = runner();
await instance.configure({});
const contributions = await instance.contributions();
expect(contributions.middleware).toEqual([]);
expect(contributions.migrations).toEqual([]);
});
test("database authz config contributes middleware and dialect migration", async () => {
const instance = runner();
const config: Record<string, unknown> = {
db: { driver: "postgres", url: "postgres://example" },
authz: {
store: "database",
middleware: true,
migrations: true,
defaultRoles: { signup: ["manager"] },
},
};
await instance.configure(config);
const contributions = await instance.contributions();
expect(contributions.middleware).toHaveLength(1);
expect(contributions.migrations).toHaveLength(1);
expect(contributions.migrations[0]?.source).toContain("SERIAL PRIMARY KEY");
expect(config.authz).toMatchObject({ strict: true });
});
test("database authz config refuses a missing database", async () => {
const instance = runner();
await expect(instance.configure({ authz: { store: "database" } })).rejects.toThrow(
"requires config.db",
);
});
+6
View File
@@ -26,6 +26,12 @@ describe("defineAuthz", () => {
expect(() => defineAuthz({ permissions: { "post:*": {} } })).toThrow(/wildcard/i); 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", () => { test("rejects a role granting an unknown-shaped entry", () => {
expect(() => defineAuthz({ roles: { editor: [""] } })).toThrow(/role 'editor'/i); expect(() => defineAuthz({ roles: { editor: [""] } })).toThrow(/role 'editor'/i);
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/captcha", "name": "@wrnexus/captcha",
"version": "0.8.11", "version": "0.8.14",
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.", "description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
"type": "module", "type": "module",
"sideEffects": false, "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", "name": "@wrnexus/cli",
"version": "0.8.47", "version": "0.8.64",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./workspace": "./src/workspace.ts" "./workspace": "./src/workspace.ts",
"./test-setup": "./src/test-setup.ts"
}, },
"bin": { "bin": {
"wrnexus": "src/index.ts" "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";`); 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 manifestRoutes = (routes: Route[]): string => {
const parts = routes.map((r) => { const parts = routes.map((r) => {
const v = `m${counter++}`; const v = `m${counter++}`;
@@ -754,6 +763,15 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
.join(", "); .join(", ");
if (router.services.length) console.log(`✓ RPC services: ${router.services.length}`); 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 // Authorization declarations again, this time for ProdOptions.authz — a
// SEPARATE set of static imports of the exact same files (harmless; ES // SEPARATE set of static imports of the exact same files (harmless; ES
// modules are evaluated once and shared across every importer), statically // modules are evaluated once and shared across every importer), statically
@@ -789,6 +807,7 @@ await createProductionServer(
components: [${componentsLit}], components: [${componentsLit}],
layouts: [${layoutsLit}], layouts: [${layoutsLit}],
services: [${servicesLit}], services: [${servicesLit}],
queues: [${queuesLit}],
}, },
{ {
reactivePath: join(import.meta.dir, "reactive.js"), reactivePath: join(import.meta.dir, "reactive.js"),
@@ -806,6 +825,7 @@ await createProductionServer(
i18n: ${i18n ? JSON.stringify(i18n) : "undefined"}, i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
db: ${config.db ? JSON.stringify(config.db) : "undefined"}, db: ${config.db ? JSON.stringify(config.db) : "undefined"},
databases: ${config.databases ? JSON.stringify(config.databases) : "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"}, storage: ${config.storage ? JSON.stringify(config.storage) : "undefined"},
${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""} ${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""}
${ ${
+31
View File
@@ -0,0 +1,31 @@
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], {
cwd: root,
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
});
const code = await child.exited;
if (code !== 0) throw new Error(`WRN-CHECK: '${name}' failed with exit code ${code}`);
}
/** Canonical generated-artifact-aware application verification pipeline. */
export async function runCheck(appRoot: string): Promise<void> {
const root = resolve(appRoot);
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")) as {
scripts?: Record<string, string>;
};
assertReachability(root);
await runBuild(root);
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);
}
}
+22 -3
View File
@@ -7,6 +7,7 @@
import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { runAdd } from "./add.ts";
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts"; import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
import { currentCliVersion } from "./update-notifier.ts"; import { currentCliVersion } from "./update-notifier.ts";
@@ -93,7 +94,7 @@ Thumbs.db
"doctor": "wrnexus doctor .", "doctor": "wrnexus doctor .",
"analyze": "wrnexus analyze .", "analyze": "wrnexus analyze .",
"inspect": "wrnexus inspect packages .", "inspect": "wrnexus inspect packages .",
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check" "check": "wrnexus check ."
}, },
"dependencies": { "dependencies": {
"@wrnexus/ai": "${scaffoldFrameworkRange}", "@wrnexus/ai": "${scaffoldFrameworkRange}",
@@ -227,8 +228,13 @@ export default tseslint.config(
dist/ dist/
.wrnexus/ .wrnexus/
**/.wrnexus/ **/.wrnexus/
.superpowers/
.claude/
*.log *.log
CLAUDE.md CLAUDE.md
app/routes.gen.ts
app/types/*.generated.*
app/db/queries.gen.ts
`, `,
".editorconfig": `root = true ".editorconfig": `root = true
@@ -667,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) { if (!name) {
console.error("Usage: wrnexus create <app-name>"); console.error("Usage: wrnexus create <app-name>");
process.exit(1); 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(`✓ Created ${name}`);
console.log(`\nNext steps:`); console.log(`\nNext steps:`);
+3 -2
View File
@@ -14,7 +14,7 @@
* (`databases.<name>`), with files under `app/db/<name>/`. * (`databases.<name>`), with files under `app/db/<name>/`.
*/ */
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"; import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { basename, join, resolve } from "node:path"; import { basename, join, resolve } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { import {
@@ -23,6 +23,7 @@ import {
type PackageMigrationDefinition, type PackageMigrationDefinition,
} from "@wrnexus/plugin"; } from "@wrnexus/plugin";
import { loadAppConfig, type AppConfig } from "@wrnexus/styles"; import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
import { writeGeneratedFile } from "./write-generated.ts";
import { import {
generateQueriesFile, generateQueriesFile,
analyzeMigrations, analyzeMigrations,
@@ -156,7 +157,7 @@ export async function regenerateQueries(
.flatMap((f) => parseQueries(readFileSync(join(queriesDir, f), "utf8"))); .flatMap((f) => parseQueries(readFileSync(join(queriesDir, f), "utf8")));
const refs = await loadModelRefs(dbBase); const refs = await loadModelRefs(dbBase);
const code = generateQueriesFile(queries, refs, dialectOf(driver)); const code = generateQueriesFile(queries, refs, dialectOf(driver));
writeFileSync(join(dbBase, "queries.gen.ts"), code, "utf8"); writeGeneratedFile(join(dbBase, "queries.gen.ts"), code);
return queries.length; return queries.length;
} }
+40
View File
@@ -30,6 +30,46 @@ export interface GeneratedFile {
content: string; 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 { function toPascalCase(name: string): string {
return name return name
.split(/[^A-Za-z0-9]+/) .split(/[^A-Za-z0-9]+/)
+35 -1
View File
@@ -60,6 +60,7 @@ Usage:
wrnexus generate types [app-dir] Generate application-wide route/component/key types wrnexus generate types [app-dir] Generate application-wide route/component/key types
wrnexus routes [app-dir] Generate typed named routes wrnexus routes [app-dir] Generate typed named routes
wrnexus typecheck [app-dir] Generate types and check TypeScript plus every .wrn file wrnexus typecheck [app-dir] Generate types and check TypeScript plus every .wrn file
wrnexus check [app-dir] Build/generate, typecheck, lint, test, and format-check
wrnexus mobile add <package...> Install Capacitor or Expo native packages wrnexus mobile add <package...> Install Capacitor or Expo native packages
wrnexus mobile compile Compile .wrn pages into native Expo routes wrnexus mobile compile Compile .wrn pages into native Expo routes
wrnexus native list List cross-platform native capabilities wrnexus native list List cross-platform native capabilities
@@ -112,6 +113,11 @@ Update options: --dry-run previews changes; --no-verify skips post-update check/
async function main(): Promise<void> { async function main(): Promise<void> {
const [command, ...rest] = process.argv.slice(2); const [command, ...rest] = process.argv.slice(2);
if (command === "--version" || command === "-v" || command === "version") {
console.log(currentCliVersion());
return;
}
notifyIfUpdateAvailable(currentCliVersion(), command); notifyIfUpdateAvailable(currentCliVersion(), command);
switch (command) { switch (command) {
@@ -166,7 +172,7 @@ async function main(): Promise<void> {
break; break;
} }
case "create": case "create":
createApp(rest[0] ?? ""); createApp(rest.find((value) => !value.startsWith("--")) ?? "", rest);
break; break;
case "workspace": { case "workspace": {
const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts"); const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts");
@@ -191,6 +197,11 @@ async function main(): Promise<void> {
} }
case "generate": case "generate":
case "g": { case "g": {
if (rest[0] === "resource") {
const { runGenerateResource } = await import("./generate.ts");
runGenerateResource(".", rest[1]);
break;
}
if (rest[0] === "routes") { if (rest[0] === "routes") {
const { regenerateRoutes } = await import("./routes.ts"); const { regenerateRoutes } = await import("./routes.ts");
const n = regenerateRoutes(join(resolve(rest[1] ?? "."), "app")); const n = regenerateRoutes(join(resolve(rest[1] ?? "."), "app"));
@@ -239,6 +250,14 @@ async function main(): Promise<void> {
else console.log("✓ Application types are valid"); else console.log("✓ Application types are valid");
break; break;
} }
case "check": {
const appRoot = rest.find((arg) => !arg.startsWith("--")) ?? ".";
bootstrapProfile(appRoot, "production", rest);
const { runCheck } = await import("./check.ts");
await runCheck(appRoot);
console.log("✓ Application check passed");
break;
}
case "eject": { case "eject": {
const { runEject } = await import("./eject.ts"); const { runEject } = await import("./eject.ts");
const args = rest.filter((a) => !a.startsWith("--")); const args = rest.filter((a) => !a.startsWith("--"));
@@ -298,6 +317,21 @@ async function main(): Promise<void> {
if (!result.ok) process.exitCode = 1; if (!result.ok) process.exitCode = 1;
break; 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": { case "security": {
const values = rest.filter((value) => !value.startsWith("--")); const values = rest.filter((value) => !value.startsWith("--"));
const { runSecurityCommand } = await import("./security-command.ts"); 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")}`,
);
}
+2 -2
View File
@@ -4,14 +4,14 @@
* startup; also exposed via `wrnexus generate routes`. * startup; also exposed via `wrnexus generate routes`.
*/ */
import { writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { buildRouter, generateRoutesFile } from "@wrnexus/router"; import { buildRouter, generateRoutesFile } from "@wrnexus/router";
import { writeGeneratedFile } from "./write-generated.ts";
/** Regenerate `app/routes.gen.ts`. Returns the number of page routes. */ /** Regenerate `app/routes.gen.ts`. Returns the number of page routes. */
export function regenerateRoutes(appDir: string): number { export function regenerateRoutes(appDir: string): number {
const router = buildRouter(appDir); const router = buildRouter(appDir);
const code = generateRoutesFile(router.pages); const code = generateRoutesFile(router.pages);
writeFileSync(join(appDir, "routes.gen.ts"), code, "utf8"); writeGeneratedFile(join(appDir, "routes.gen.ts"), code);
return router.pages.length; return router.pages.length;
} }
+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; level?: TestLevel;
files: string[]; files: string[];
setup?: { command: string; args: string[] }; setup?: { command: string; args: string[] };
env?: Record<string, string>;
} }
function shardFiles(files: string[], value?: 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 root = resolve(appRoot);
const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel)); const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
const watch = args.includes("--watch"); 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 shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium") const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
.split(",") .split(",")
@@ -120,10 +126,18 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
); );
return { return {
command: process.execPath, command: process.execPath,
args: ["test", ...(watch ? ["--watch"] : []), ...(pattern ? files : []), ...passthrough], args: [
"test",
"--preload",
setupModule,
...(watch ? ["--watch"] : []),
...(pattern ? files : []),
...passthrough,
],
cwd: root, cwd: root,
level, level,
files, 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; process.exitCode = 1;
return null; 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) { if (plan.setup) {
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd }); const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
setup.on("exit", (code, signal) => { setup.on("exit", (code, signal) => {
+3 -2
View File
@@ -9,6 +9,7 @@ import { generate, generateTargets } from "@wrnexus/compiler";
import { regenerateRoutes } from "./routes.ts"; import { regenerateRoutes } from "./routes.ts";
import { loadAppConfig } from "@wrnexus/styles"; import { loadAppConfig } from "@wrnexus/styles";
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin"; import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
import { writeGeneratedFile } from "./write-generated.ts";
function files(root: string, predicate: (path: string) => boolean): string[] { function files(root: string, predicate: (path: string) => boolean): string[] {
if (!existsSync(root)) return []; if (!existsSync(root)) return [];
@@ -309,7 +310,7 @@ declare namespace WRNexusGenerated {
`; `;
mkdirSync(typeDir, { recursive: true }); mkdirSync(typeDir, { recursive: true });
const output = join(typeDir, "wrnexus.generated.d.ts"); const output = join(typeDir, "wrnexus.generated.d.ts");
writeFileSync(output, code, "utf8"); writeGeneratedFile(output, code);
// `.d.ts` contents are exempt from checking under `skipLibCheck: true` (set in the // `.d.ts` contents are exempt from checking under `skipLibCheck: true` (set in the
// repo/app tsconfig), so the per-block assertions are written into a real `.ts` file // repo/app tsconfig), so the per-block assertions are written into a real `.ts` file
// instead — only genuine `.ts`/`.tsx` sources are compiled and checked. // instead — only genuine `.ts`/`.tsx` sources are compiled and checked.
@@ -321,7 +322,7 @@ declare namespace WRNexusGenerated {
${apiBlockAssertions(pageAsts, apiContracts, app)} ${apiBlockAssertions(pageAsts, apiContracts, app)}
export {}; export {};
`; `;
writeFileSync(join(typeDir, "wrnexus.generated.api-checks.ts"), apiChecksCode, "utf8"); writeGeneratedFile(join(typeDir, "wrnexus.generated.api-checks.ts"), apiChecksCode);
writePluginArtifacts(root, pluginContributions); writePluginArtifacts(root, pluginContributions);
return { return {
file: relative(root, output).replace(/\\/g, "/"), file: relative(root, output).replace(/\\/g, "/"),
+13 -4
View File
@@ -63,14 +63,16 @@ const execFileAsync = promisify(execFile);
/** Resolve every installed WRNexus dependency independently and in parallel. */ /** Resolve every installed WRNexus dependency independently and in parallel. */
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> { 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 unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
const entries = await Promise.all( const entries = await Promise.all(
unique.map(async (name) => { unique.map(async (name) => {
try { try {
const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], { const command = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "npm";
encoding: "utf8", 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 parsed = JSON.parse(stdout.trim()) as string | string[];
const version = Array.isArray(parsed) ? parsed.at(-1) : parsed; const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version) 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>() ? new Map<string, string>()
: await latestPackageVersions(dependencyNames); : 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)" : ""}`); console.log(`\n ⚡ wrnexus update → migrations ${target}${dryRun ? " (dry run)" : ""}`);
if (!versionArg) { if (!versionArg) {
console.log( console.log(
+11
View File
@@ -0,0 +1,11 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs";
/** Write generated UTF-8 output only when bytes changed, preserving mtimes on no-op builds. */
export function writeGeneratedFile(path: string, content: string): boolean {
const normalized = content.replace(/\r\n/g, "\n");
if (existsSync(path) && readFileSync(path, "utf8").replace(/\r\n/g, "\n") === normalized) {
return false;
}
writeFileSync(path, normalized, "utf8");
return true;
}
+1 -3
View File
@@ -51,9 +51,7 @@ test("scaffoldApp includes production build and start scripts", () => {
expect(pkg.scripts.production).toBe("bun run build && bun run start"); expect(pkg.scripts.production).toBe("bun run build && bun run start");
expect(pkg.scripts.typecheck).toBe("tsc --noEmit"); expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
expect(pkg.scripts.test).toBe("wrnexus test ."); expect(pkg.scripts.test).toBe("wrnexus test .");
expect(pkg.scripts.check).toBe( expect(pkg.scripts.check).toBe("wrnexus check .");
"bun run typecheck && bun run lint && bun run test && bun run format:check",
);
} finally { } finally {
rmSync(parent, { recursive: true, force: true }); rmSync(parent, { recursive: true, force: true });
} }
+13 -1
View File
@@ -1,5 +1,5 @@
import { test, expect } from "bun:test"; 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", () => { test("scaffold page: kebab file, PascalCase page name", () => {
const f = scaffold("page", "about-us"); 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("page", "home.wrn").path).toBe("pages/home.wrn");
expect(scaffold("api", "ping.ts").path).toBe("api/ping.ts"); 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 () => { test("keeps the unfiltered legacy command", async () => {
const plan = createTestPlan(await fixture(), ["--watch"]); const plan = createTestPlan(await fixture(), ["--watch"]);
expect(plan.level).toBeUndefined(); 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 { afterEach, expect, test } from "bun:test";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join, resolve } from "node:path";
import { compareVersions, notifyIfUpdateAvailable } from "../src/update-notifier.ts"; import {
compareVersions,
currentCliVersion,
notifyIfUpdateAvailable,
} from "../src/update-notifier.ts";
const originalCache = process.env.WRNEXUS_UPDATE_CACHE; 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 }); 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", "name": "@wrnexus/compiler",
"version": "0.8.14", "version": "0.8.22",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+153 -29
View File
@@ -138,6 +138,29 @@ function isHtmlBooleanAttribute(name: string): boolean {
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase()); return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
} }
/*
* Constraint attributes that must be absent rather than empty. `pattern` is the
* dangerous one: an empty pattern compiles to a regex matching only the empty
* string, so every typed value becomes invalid and the form silently refuses to
* submit. A component declaring `pattern: string = ""` and rendering
* `pattern="{pattern}"` therefore breaks every input that did not ask for a
* pattern. The rest are inert when empty, but carry no meaning either.
*/
const OMIT_WHEN_EMPTY_ATTRIBUTES = new Set([
"pattern",
"minlength",
"maxlength",
"min",
"max",
"step",
"inputmode",
"accept",
]);
function isOmitWhenEmptyAttribute(name: string): boolean {
return OMIT_WHEN_EMPTY_ATTRIBUTES.has(name.toLowerCase());
}
const URL_ATTRIBUTES = new Set([ const URL_ATTRIBUTES = new Set([
"href", "href",
"src", "src",
@@ -203,9 +226,11 @@ function renderAttr(attr: Attr): string {
case "csrText": case "csrText":
return ""; return "";
default: default:
return attr.boolean if (attr.boolean) return ` ${attr.name}`;
? ` ${attr.name}` // An empty constraint attribute is worse than absent -- see
: ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`; // OMIT_WHEN_EMPTY_ATTRIBUTES.
if (attr.value === "" && isOmitWhenEmptyAttribute(attr.name)) return "";
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
} }
} }
@@ -258,6 +283,32 @@ function componentEventAttribute(name: string): string {
return `data-wrn-out-${name}`; 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 { function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
let found = false; let found = false;
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => { const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
@@ -280,6 +331,7 @@ function renderAttrs(
reactive: PageReactive | null = null, reactive: PageReactive | null = null,
dynamicExpressions?: string[], dynamicExpressions?: string[],
): string { ): string {
attrs = expandBindings(attrs);
let bindIndex = 0; let bindIndex = 0;
const rendered = attrs const rendered = attrs
.map((attr) => { .map((attr) => {
@@ -423,22 +475,22 @@ function bakeLoopAttr(raw: string, typed = false): string {
} }
/** Render one loop-body node to template-literal source (nested loops inline). */ /** Render one loop-body node to template-literal source (nested loops inline). */
function renderLoopBody(node: ViewNode): string { function renderLoopBody(node: ViewNode, locals: string[] = []): string {
if (node.type === "text") { if (node.type === "text") {
return bakeLoopText(node.value); return bakeLoopText(node.value);
} }
if (node.type === "each") { if (node.type === "each") {
return compileEachExpr(node); return compileEachExpr(node, locals);
} }
if (node.type === "if") { if (node.type === "if") {
return compileIfExpr(node); return compileIfExpr(node, locals);
} }
const componentTag = isComponentTag(node.tag); const componentTag = isComponentTag(node.tag);
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component") .filter((attr) => attr.name !== "data-component")
.map((attr) => { .map((attr) => {
const name = attr.event const name = attr.event
@@ -455,7 +507,16 @@ function renderLoopBody(node: ViewNode): string {
}) })
.join(""); .join("");
const inner = node.children.map(renderLoopBody).join(""); // A handler expression is emitted as text and evaluated at event time, so any
// `{#each}` variable it names has to travel with the element. The runtime
// resolves them with closest("[data-wrn-loop-locals]"). Only elements that
// actually bind an event need it -- marking every node would bloat the HTML.
const localsAttr =
locals.length > 0 && node.attrs.some((attr) => attr.event) ? loopLocalsAttr(locals) : "";
const inner = node.children.map((child) => renderLoopBody(child, locals)).join("");
const openAttrs = attrs + localsAttr;
if (node.tag === "Static") return inner; if (node.tag === "Static") return inner;
if (node.tag === "Dynamic") if (node.tag === "Dynamic")
@@ -511,7 +572,7 @@ function renderLoopBody(node: ViewNode): string {
if (island) return escLit(island); if (island) return escLit(island);
return ( return (
escLit(`<div data-component="${attrEscape(node.tag)}"`) + escLit(`<div data-component="${attrEscape(node.tag)}"`) +
attrs + openAttrs +
escLit(">") + escLit(">") +
inner + inner +
escLit("</div>") escLit("</div>")
@@ -519,10 +580,20 @@ function renderLoopBody(node: ViewNode): string {
} }
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) { if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return escLit(`<${node.tag}`) + attrs + escLit(">"); return escLit(`<${node.tag}`) + openAttrs + escLit(">");
} }
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`); return escLit(`<${node.tag}`) + openAttrs + escLit(">") + inner + escLit(`</${node.tag}>`);
}
/**
* Build the ` data-wrn-loop-locals="..."` attribute for a page-rendered loop
* body. Deliberately not passed through escLit: the `${...}` must stay live so
* the values are encoded at render time.
*/
function loopLocalsAttr(locals: string[]): string {
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
} }
/** /**
@@ -530,11 +601,13 @@ function renderLoopBody(node: ViewNode): string {
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a * that iterates the (server-evaluated) list and joins the per-item body. `list` is a
* JS expression evaluated where `ssr` data bindings are in scope as raw named values. * JS expression evaluated where `ssr` data bindings are in scope as raw named values.
*/ */
function compileEachExpr(node: EachNode): string { function compileEachExpr(node: EachNode, outerLocals: string[] = []): string {
const item = node.item; const item = node.item;
const index = node.index ?? "__wi"; const index = node.index ?? "__wi";
const body = node.body.map(renderLoopBody).join(""); // A nested loop can reference the outer loop's variables too.
const empty = node.empty.map(renderLoopBody).join(""); const locals = [...outerLocals, item, index];
const body = node.body.map((child) => renderLoopBody(child, locals)).join("");
const empty = node.empty.map((child) => renderLoopBody(child, outerLocals)).join("");
return ( return (
"${(() => { const __wl = Array.isArray(" + "${(() => { const __wl = Array.isArray(" +
node.list + node.list +
@@ -557,11 +630,11 @@ function compileEachExpr(node: EachNode): string {
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither). * that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
* Conditions are JS expressions evaluated in the surrounding server scope. * Conditions are JS expressions evaluated in the surrounding server scope.
*/ */
function compileIfExpr(node: IfNode): string { function compileIfExpr(node: IfNode, locals: string[] = []): string {
let expr = "``"; // no matching branch → empty string let expr = "``"; // no matching branch → empty string
for (let k = node.branches.length - 1; k >= 0; k--) { for (let k = node.branches.length - 1; k >= 0; k--) {
const b = node.branches[k]!; const b = node.branches[k]!;
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`"; const bodySrc = "`" + b.body.map((child) => renderLoopBody(child, locals)).join("") + "`";
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr; expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
} }
return "${" + expr + "}"; return "${" + expr + "}";
@@ -759,9 +832,9 @@ function renderPageComponentInvocation(
const island = islandMarkerFor(node); const island = islandMarkerFor(node);
if (island) return island; if (island) return island;
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component") .filter((attr) => attr.name !== "data-component")
.map((attr) => renderPageComponentAttr(attr, loops)) .map((attr) => renderPageComponentAttr(attr, loops, reactive))
.join(""); .join("");
const inner = node.children const inner = node.children
@@ -781,7 +854,7 @@ function renderNestedComponentInvocation(
let bindIndex = 0; let bindIndex = 0;
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((attr) => attr.name !== "data-component") .filter((attr) => attr.name !== "data-component")
.map((attr) => { .map((attr) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name); const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
@@ -1619,6 +1692,18 @@ function generateInner(ast: PageAst): string {
if (needsRuntimeHelpers) { if (needsRuntimeHelpers) {
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`); out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
out.push(ssrRuntimeSource()); out.push(ssrRuntimeSource());
// Loop bodies that bind an event carry their {#each} locals in an encoded
// attribute. Emitted only when the view actually produced one, so a page
// without handlers in a loop keeps the smaller prelude -- but it MUST be
// emitted whenever the marker is, or the render throws on an undefined
// function instead of the handler throwing on an undefined variable.
if (body.includes("__wrnexusEncodeLoopLocals(")) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
const json = JSON.stringify(value);
const buffer = (globalThis as { Buffer?: { from(i: string, e: string): { toString(e: string): string } } }).Buffer;
return buffer ? buffer.from(json, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(json)));
}`);
}
out.push( out.push(
`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`, `const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`,
); );
@@ -1772,6 +1857,8 @@ ${declarations}
${ ${
visible.length visible.length
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]); ? ` 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(", ")} };` return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
: "" : ""
} }
@@ -2193,7 +2280,7 @@ function renderClientControlTemplate(nodes: ViewNode[]): string {
const componentTag = isComponentTag(node.tag); const componentTag = isComponentTag(node.tag);
let bindIndex = 0; let bindIndex = 0;
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.map((attribute) => { .map((attribute) => {
const name = attribute.event const name = attribute.event
? componentTag ? componentTag
@@ -2235,24 +2322,27 @@ function encodeClientControl(value: unknown): string {
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string { function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
let expression = "``"; let expression = "``";
const clientBranches: string[] = [];
for (let index = node.branches.length - 1; index >= 0; index--) { for (let index = node.branches.length - 1; index >= 0; index--) {
const branch = node.branches[index]!; const branch = node.branches[index]!;
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join(""); const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
const bodyExpression = "`" + body + "`"; const bodyExpression = "`" + body + "`";
clientBranches.unshift(
`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`,
);
expression = expression =
branch.cond === null branch.cond === null
? bodyExpression ? bodyExpression
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`; : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
} }
const definition = encodeClientControl( // Render all branches on the server into an inert, encoded definition.
node.branches.map((branch) => ({ // A branch first selected in the browser therefore contains the real nested
cond: branch.cond, // component HTML and scope metadata, not a data-component placeholder.
body: renderClientControlTemplate(branch.body), const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
})),
);
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`; return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
} }
@@ -2431,7 +2521,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
(attribute) => attribute.name === "data-component", (attribute) => attribute.name === "data-component",
); );
const attrs = node.attrs const attrs = expandBindings(node.attrs)
.filter((a) => a.name !== "class" && !a.name.startsWith("class:")) .filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
.map((a) => { .map((a) => {
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name); const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
@@ -2505,6 +2595,19 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
isExplicitComponentMount && wholeExpression isExplicitComponentMount && wholeExpression
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}` ? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
: compileAttrValue(a.value, elementContext); : compileAttrValue(a.value, elementContext);
/*
* A constraint attribute whose value is only known at render time (a
* component prop defaulting to "") must be decided at render time, so
* the whole attribute -- name included -- is emitted by the helper.
* Component mounts are excluded: there the value is a prop being passed
* down, not an HTML attribute being written.
*/
if (isOmitWhenEmptyAttribute(a.name) && !isExplicitComponentMount) {
if (a.value === "") return "";
return `\${__wrnOptionalAttr(${JSON.stringify(a.name)}, \`${compiledValue}\`)}`;
}
const rendered = ` ${a.name}="${compiledValue}"`; const rendered = ` ${a.name}="${compiledValue}"`;
const referencesState = exprRefsComponentReactiveValue(a.value, ctx); const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
@@ -2880,6 +2983,10 @@ function __wrnHtml(v: unknown): string {
); );
} }
function __wrnOptionalAttr(name: string, value: string): string {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v: unknown): string { function __wrnAttr(v: unknown): string {
return String(v == null ? "" : v).replace( return String(v == null ? "" : v).replace(
/[&<>"]/g, /[&<>"]/g,
@@ -2961,6 +3068,9 @@ function __wrnRaw(v: unknown): string {
} }
if (needsScope) { 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 { out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) { if (value === undefined) {
return "undefined"; return "undefined";
@@ -3046,6 +3156,10 @@ function __wrnHtml(v: unknown): string {
); );
} }
function __wrnOptionalAttr(name: string, value: string): string {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v: unknown): string { function __wrnAttr(v: unknown): string {
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;", c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;",
@@ -3058,7 +3172,11 @@ function __wrnProp(v: unknown): string {
return __wrnAttr(value); return __wrnAttr(value);
} }
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string { function renderPageComponentAttr(
attr: Attr,
dynamicExpressions: string[],
_reactive: PageReactive | null,
): string {
if (attr.event) { if (attr.event) {
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`; return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
} }
@@ -3076,6 +3194,12 @@ function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): stri
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`); dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`; 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 { export class NativeCompileError extends Error {
constructor(message: string) { constructor(message: string) {
@@ -68,11 +68,11 @@ function expression(value: string): string | null {
function textJsx(value: string): string { function textJsx(value: string): string {
const pieces: string[] = []; const pieces: string[] = [];
let last = 0; let last = 0;
for (const match of value.matchAll(/\{([^{}]+)\}/g)) { for (const segment of extractInterpolations(value)) {
if (match.index! > last) pieces.push(value.slice(last, match.index)); if (segment.start > last) pieces.push(value.slice(last, segment.start));
const expr = match[1]!.trim(); const expr = segment.expression;
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`); 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)); pieces.push(value.slice(last));
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;")); return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "&lt;" : "&gt;"));
@@ -133,6 +133,10 @@ function __wrnHtml(v: unknown): string {
); );
} }
function __wrnOptionalAttr(name: string, value: string): string {
return value === "" ? "" : " " + name + '="' + value + '"';
}
function __wrnAttr(v: unknown): string { function __wrnAttr(v: unknown): string {
return String(v == null ? "" : v).replace( return String(v == null ? "" : v).replace(
/[&<>"]/g, /[&<>"]/g,
@@ -207,6 +211,10 @@ function __wrnRaw(v: unknown): string {
return String(v == null ? "" : v); return String(v == null ? "" : v);
} }
function __wrnexusEncodeControl(value: unknown): string {
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}
function __wrnexusSerializeScopeValue(value: unknown): string { function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) { if (value === undefined) {
return "undefined"; return "undefined";
@@ -56,6 +56,17 @@ test("named loads support memoized dependencies and deferred execution", () => {
expect(code).toContain('return { "audit": __values[0] }'); 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", () => { test("load dependency cycles and cross-phase server dependencies fail compilation", () => {
expect(() => expect(() =>
parse( parse(
+29
View File
@@ -1070,6 +1070,25 @@ component Banner {
expect(output).toContain("visible"); expect(output).toContain("visible");
expect(output).toContain("Visible"); expect(output).toContain("Visible");
expect(output).toContain("Hidden"); 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", () => { test("if and each blocks emit browser control metadata while preserving SSR", () => {
@@ -1132,6 +1151,16 @@ page Home {
expect(output).toContain("__wrnexusPropAttr(["); 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", () => { test("nested component props retain parent-owned reactive bindings", () => {
const output = generate( const output = generate(
parse(`component Parent { parse(`component Parent {
@@ -0,0 +1,63 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { compile } from "../src/index.ts";
/**
* `pattern=""` is not inert. An empty pattern compiles to a regex that matches
* only the empty string, so EVERY typed value becomes invalid and the form
* silently refuses to submit -- no error, no request.
*
* `@wrnexus/ui`'s input declares `pattern: string = ""` and renders
* `pattern="{pattern}"`, so every input that did not opt into a pattern shipped
* one that could never match. This broke sign-up in a real app.
*
* Attributes reach the output through two different emitters, and both matter:
* a component's interpolated attribute is baked at render time via
* `__wrnAttr`, while a page's static element is serialized by `renderAttr`.
*/
const uiInput = join(import.meta.dir, "..", "..", "ui", "components", "input.wrn");
function render(view: string): string {
return compile(`page P {\n view { ${view} }\n}\n`, "P.wrn").code;
}
test("the real ui input never emits a bare pattern attribute", () => {
const code = compile(readFileSync(uiInput, "utf8"), uiInput).code;
// The broken shape: the attribute is always present, empty or not.
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
// The fixed shape: the attribute itself is decided at render time.
expect(code).toContain("__wrnOptionalAttr");
});
test("an interpolated constraint attribute on a component is emitted through the helper", () => {
const source = `component Field {\n props { pattern: string = "" }\n view { <input type="text" pattern="{pattern}" /> }\n}\n`;
const code = compile(source, "Field.wrn").code;
expect(code).toContain("__wrnOptionalAttr");
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
});
test("a static empty constraint attribute on a page element is dropped", () => {
const code = render(`<input type="email" pattern="" minlength="" />`);
expect(code).not.toContain('pattern=""');
expect(code).not.toContain('minlength=""');
});
test("a populated constraint attribute survives on both paths", () => {
expect(render(`<input type="text" pattern="[0-9]+" />`)).toContain('pattern="[0-9]+"');
const component = compile(
`component F {\n props { p: string = "x" }\n view { <input pattern="{p}" /> }\n}\n`,
"F.wrn",
).code;
expect(component).toContain("pattern");
});
test("attributes outside the constraint set keep their empty values", () => {
// An empty value or class is meaningful and must survive untouched.
const code = render(`<input type="text" value="" class="" />`);
expect(code).toContain('value=""');
});
@@ -0,0 +1,120 @@
import { expect, test } from "bun:test";
import { generate, parse } from "../src/index.ts";
// A `{#each}` inside a COMPONENT emitted `data-wrn-loop-locals`, so a handler
// could reference the loop variable. The same view inside a PAGE emitted the
// handler text verbatim with no locals marker, so the loop variable was not
// defined at click time and the handler threw ReferenceError -- with a green
// build and green tests, because nothing renders the page in a browser during
// a build. The runtime resolves locals with closest("[data-wrn-loop-locals]"),
// so emitting the marker is all that is required.
const view = `
{#each packs as pack}
<button @click="buyPack(pack.code)">{pack.code}</button>
{/each}
`;
const body = `
state packs = [{ code: "small" }]
functions { client async function buyPack(c) { console.log(c) } }
view {${view}}
`;
test("a page {#each} exposes loop locals to an event handler", () => {
const code = generate(parse(`page P {${body}}`));
expect(code).toContain("data-wrn-loop-locals");
// The item and the implicit index both have to travel, since a handler may
// reference either.
expect(code).toContain('"pack": pack');
});
test("a component {#each} still exposes loop locals to an event handler", () => {
const code = generate(parse(`component C {${body}}`));
expect(code).toContain("data-wrn-loop-locals");
expect(code).toContain('"pack": pack');
});
test("a named loop index is exposed to a page event handler", () => {
const code = generate(
parse(`page P {
state packs = [{ code: "small" }]
functions { client async function pick(c) { console.log(c) } }
view {
{#each packs as pack, i}
<button @click="pick(i)">{pack.code}</button>
{/each}
}
}`),
);
expect(code).toContain('"i": i');
});
// Only elements that actually bind an event need the marker; adding it to every
// element in a loop body would bloat the HTML for no benefit.
test("a page loop body element with no handler gets no locals marker", () => {
const code = generate(
parse(`page P {
state packs = [{ code: "small" }]
view {
{#each packs as pack}
<span>{pack.code}</span>
{/each}
}
}`),
);
expect(code).not.toContain("data-wrn-loop-locals");
});
// Emitting the marker without defining its encoder would turn a ReferenceError
// on the loop variable into a ReferenceError on the encoder -- at render time
// rather than click time, so strictly worse.
test("a page that emits loop locals also defines the encoder", () => {
const code = generate(parse(`page P {${body}}`));
expect(code).toContain("data-wrn-loop-locals");
expect(code).toContain("function __wrnexusEncodeLoopLocals");
});
test("a page with no loop-local markers does not define the encoder", () => {
const code = generate(
parse(`page P {
state packs = [{ code: "small" }]
view { <span>{packs.length}</span> }
}`),
);
expect(code).not.toContain("__wrnexusEncodeLoopLocals");
});
// The tests above assert the marker is EMITTED. This one executes the generated
// module and asserts it RENDERS with the loop's real values -- generated text
// that reads correctly can still render wrong, and the whole point of the
// marker is what the runtime finds in the DOM at click time.
test("a page loop handler's locals render as real, decodable values", async () => {
const code = generate(
parse(`page Packs {
state packs = [{ code: "small" }, { code: "large" }]
functions { client async function pick(c) { console.log(c) } }
view {
{#each packs as pack}
<button @click="pick(pack.code)">{pack.code}</button>
{/each}
}
}`),
).replace(/^import \{ buildApiRequest[^\n]*\n/m, "");
const js = new Bun.Transpiler({ loader: "ts" }).transformSync(code);
const mod = await import("data:text/javascript;base64," + Buffer.from(js).toString("base64"));
const html: string = await mod.default({
req: new Request("http://x/"),
cookies: {},
session: {},
});
const markers = [...html.matchAll(/data-wrn-loop-locals="([^"]+)"/g)].map((m) => m[1]!);
expect(markers.length).toBe(2);
const decoded = markers.map((m) => JSON.parse(Buffer.from(m, "base64").toString("utf8")));
expect(decoded[0].pack).toEqual({ code: "small" });
expect(decoded[1].pack).toEqual({ code: "large" });
expect(decoded[0].__wi).toBe(0);
expect(decoded[1].__wi).toBe(1);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.8.10", "version": "0.8.12",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+41 -6
View File
@@ -22,7 +22,20 @@ import {
/** Translate a key for the active language, interpolating `{param}` placeholders. */ /** Translate a key for the active language, interpolating `{param}` placeholders. */
export type TFunction = (key: string, params?: Record<string, string | number>) => string; export type TFunction = (key: string, params?: Record<string, string | number>) => string;
export type Context = { export interface PaginationOptions {
defaultLimit?: number;
maxLimit?: number;
}
export interface PaginationInput {
limit: number;
cursor?: string;
}
export interface Context<
Params extends Record<string, string> = Record<string, string>,
User = unknown,
> {
/** The raw incoming web-standard Request. */ /** The raw incoming web-standard Request. */
req: Request; req: Request;
/** Parsed URL of the request (pathname, query, etc.). */ /** Parsed URL of the request (pathname, query, etc.). */
@@ -32,17 +45,19 @@ export type Context = {
/** Translate a key for the active language (identity until the runtime sets it). */ /** Translate a key for the active language (identity until the runtime sets it). */
t: TFunction; t: TFunction;
/** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */ /** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
params: Record<string, string>; params: Params;
/** /**
* Per-request scratch space. Middleware can attach values here * Per-request scratch space. Middleware can attach values here
* (e.g. the authenticated user) and downstream handlers can read them. * (e.g. the authenticated user) and downstream handlers can read them.
*/ */
locals: Record<string, unknown>; locals: Record<string, unknown>;
/** Resource resolved by a declarative route guard, when present. */
resource?: unknown;
/** /**
* The authenticated user for this request, or null when anonymous. Populated * The authenticated user for this request, or null when anonymous. Populated
* by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`. * by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
*/ */
user?: unknown; user?: User | null;
/** Active tenant/workspace resolved by tenant middleware. */ /** Active tenant/workspace resolved by tenant middleware. */
tenant?: Tenant; tenant?: Tenant;
/** Request tracer installed by observability middleware. */ /** Request tracer installed by observability middleware. */
@@ -59,7 +74,9 @@ export type Context = {
session: SessionStore; session: SessionStore;
/** Read-only localStorage snapshot sent by the browser for CSR data bindings. */ /** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
localStorage: LocalStorageSnapshot; localStorage: LocalStorageSnapshot;
}; /** Parse and bound the standard `limit` and `cursor` query parameters. */
pagination(options?: PaginationOptions): PaginationInput;
}
/** Calls the next middleware in the chain (or the final route handler). */ /** Calls the next middleware in the chain (or the final route handler). */
export type Next = () => Promise<Response> | Response; export type Next = () => Promise<Response> | Response;
@@ -99,12 +116,15 @@ export type PageMeta = SeoConfig;
export type PageComponent = (ctx: Context) => string | Promise<string>; export type PageComponent = (ctx: Context) => string | Promise<string>;
/** Create a fresh context for an incoming request. */ /** Create a fresh context for an incoming request. */
export function createContext(req: Request, url: URL): Context { export function createContext<Params extends Record<string, string> = Record<string, string>>(
req: Request,
url: URL,
): Context<Params> {
const cookies = createCookieStore(req); const cookies = createCookieStore(req);
return { return {
req, req,
url, url,
params: {}, params: {} as Params,
locals: {}, locals: {},
lang: "", lang: "",
t: (key) => key, t: (key) => key,
@@ -113,9 +133,24 @@ export function createContext(req: Request, url: URL): Context {
// cookies get `Secure` behind a TLS-terminating proxy (matches CSRF cookies). // cookies get `Secure` behind a TLS-terminating proxy (matches CSRF cookies).
session: createSessionStore(cookies, req, undefined, url.protocol === "https:"), session: createSessionStore(cookies, req, undefined, url.protocol === "https:"),
localStorage: createLocalStorageSnapshot(req), localStorage: createLocalStorageSnapshot(req),
pagination(options = {}) {
const defaultLimit = positivePaginationInteger(options.defaultLimit, 25);
const maxLimit = positivePaginationInteger(options.maxLimit, 100);
const requested = Number(url.searchParams.get("limit"));
const limit =
Number.isInteger(requested) && requested > 0
? Math.min(requested, maxLimit)
: Math.min(defaultLimit, maxLimit);
const cursor = url.searchParams.get("cursor")?.trim() || undefined;
return { limit, cursor };
},
}; };
} }
function positivePaginationInteger(value: number | undefined, fallback: number): number {
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
}
/** Apply headers accumulated on the context, such as Set-Cookie. */ /** Apply headers accumulated on the context, such as Set-Cookie. */
export function withContextHeaders(ctx: Context, res: Response): Response { export function withContextHeaders(ctx: Context, res: Response): Response {
const headers = new Headers(res.headers); const headers = new Headers(res.headers);
+29
View File
@@ -33,6 +33,10 @@ export interface EndpointDefinition<I, O> {
input?: SchemaLike<I> | OutputSchemaLike<I>; input?: SchemaLike<I> | OutputSchemaLike<I>;
output?: SchemaLike<O> | OutputSchemaLike<O>; output?: SchemaLike<O> | OutputSchemaLike<O>;
auth?: "optional" | "required"; auth?: "optional" | "required";
/** Permission checked through an installed @wrnexus/authz middleware. */
permission?: string;
/** Resource supplied to bound authorization policies. */
resource?: (ctx: Context, input: I) => unknown | Promise<unknown>;
description?: string; description?: string;
tags?: string[]; tags?: string[];
handler(input: I, ctx: Context): O | Promise<O>; handler(input: I, ctx: Context): O | Promise<O>;
@@ -111,6 +115,31 @@ export function defineEndpoint(
: await ctx.req.json().catch(() => ({})); : await ctx.req.json().catch(() => ({}));
input = schemaValue(definition.input, resolvedInput); input = schemaValue(definition.input, resolvedInput);
} }
if (definition.permission) {
const authz = (
ctx as Context & {
authz?: {
decide(
permission: string,
resource?: unknown,
): Promise<{ allowed: boolean; reason?: string }>;
};
}
).authz;
if (!authz) {
throw new EndpointError(
500,
"AUTHZ_NOT_CONFIGURED",
"Authorization middleware is not configured.",
);
}
const resource = definition.resource ? await definition.resource(ctx, input) : undefined;
const decision = await authz.decide(definition.permission, resource);
if (!decision.allowed) {
throw new EndpointError(403, "FORBIDDEN", "Permission denied.");
}
ctx.resource = resource;
}
const rawOutput = await definition.handler(input, ctx); const rawOutput = await definition.handler(input, ctx);
const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput; const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput;
return output instanceof Response ? output : json({ data: output }); return output instanceof Response ? output : json({ data: output });
+3
View File
@@ -9,6 +9,8 @@ export type {
PageComponent, PageComponent,
SeoConfig, SeoConfig,
TFunction, TFunction,
PaginationOptions,
PaginationInput,
} from "./context.ts"; } from "./context.ts";
export { createContext, withContextHeaders } from "./context.ts"; export { createContext, withContextHeaders } from "./context.ts";
export { export {
@@ -127,6 +129,7 @@ export {
export type { export type {
CookieOptions, CookieOptions,
CookieStore, CookieStore,
TransactionCookie,
LocalStorageSnapshot, LocalStorageSnapshot,
SessionStore, SessionStore,
SessionBackend, SessionBackend,
+49 -1
View File
@@ -17,6 +17,16 @@ export interface CookieStore {
set(name: string, value: string, options?: CookieOptions): void; set(name: string, value: string, options?: CookieOptions): void;
delete(name: string, options?: CookieOptions): void; delete(name: string, options?: CookieOptions): void;
headers(): string[]; headers(): string[];
transaction<T extends Record<string, unknown> = Record<string, unknown>>(
name: string,
options?: CookieOptions,
): TransactionCookie<T>;
}
export interface TransactionCookie<T extends Record<string, unknown>> {
set(value: T): void;
consume(): T | undefined;
clear(): void;
} }
export interface SessionStore { export interface SessionStore {
@@ -259,7 +269,7 @@ export function createCookieStore(req: Request): CookieStore {
const incoming = parseCookieHeader(req.headers.get("cookie") ?? ""); const incoming = parseCookieHeader(req.headers.get("cookie") ?? "");
const outgoing: string[] = []; const outgoing: string[] = [];
return { const store: CookieStore = {
get(name) { get(name) {
return incoming[name]; return incoming[name];
}, },
@@ -287,7 +297,45 @@ export function createCookieStore(req: Request): CookieStore {
headers() { headers() {
return [...outgoing]; return [...outgoing];
}, },
transaction<T extends Record<string, unknown>>(name: string, options: CookieOptions = {}) {
const policy: CookieOptions = {
path: "/",
httpOnly: true,
sameSite: "Lax",
maxAge: 600,
secure: new URL(req.url).protocol === "https:",
...options,
}; };
return {
set(value: T) {
const json = JSON.stringify(value);
const encoded = btoa(unescape(encodeURIComponent(json)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
store.set(name, encoded, policy);
},
consume(): T | undefined {
const encoded = store.get(name);
store.delete(name, policy);
if (!encoded) return undefined;
try {
const padded = encoded
.replace(/-/g, "+")
.replace(/_/g, "/")
.padEnd(Math.ceil(encoded.length / 4) * 4, "=");
return JSON.parse(decodeURIComponent(escape(atob(padded)))) as T;
} catch {
return undefined;
}
},
clear() {
store.delete(name, policy);
},
};
},
};
return store;
} }
export function createSessionStore( export function createSessionStore(
+17
View File
@@ -0,0 +1,17 @@
import { expect, test } from "bun:test";
import { createContext } from "../src/index.ts";
test("context pagination applies defaults, bounds and cursor parsing", () => {
const request = new Request("https://example.test/items?limit=500&cursor=next-1");
const ctx = createContext(request, new URL(request.url));
expect(ctx.pagination({ defaultLimit: 25, maxLimit: 100 })).toEqual({
limit: 100,
cursor: "next-1",
});
});
test("context pagination rejects invalid limits", () => {
const request = new Request("https://example.test/items?limit=-2");
const ctx = createContext(request, new URL(request.url));
expect(ctx.pagination()).toEqual({ limit: 25, cursor: undefined });
});
@@ -0,0 +1,24 @@
import { expect, test } from "bun:test";
import { createContext, withContextHeaders } from "../src/index.ts";
test("transaction cookies preserve policy when consumed and delete exactly once", () => {
const firstRequest = new Request("https://example.test/oauth");
const first = createContext(firstRequest, new URL(firstRequest.url));
first.cookies.transaction<{ state: string }>("oauth").set({ state: "välue" });
const issued = withContextHeaders(first, new Response()).headers.get("set-cookie")!;
expect(issued).toContain("HttpOnly");
expect(issued).toContain("SameSite=Lax");
expect(issued).toContain("Secure");
const pair = issued.split(";", 1)[0]!;
const callbackRequest = new Request("https://example.test/callback", {
headers: { cookie: pair },
});
const callback = createContext(callbackRequest, new URL(callbackRequest.url));
expect(callback.cookies.transaction<{ state: string }>("oauth").consume()).toEqual({
state: "välue",
});
const consumed = withContextHeaders(callback, new Response()).headers.get("set-cookie")!;
expect(consumed).toContain("Max-Age=0");
expect(consumed).toContain("Path=/");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.8.25", "version": "0.8.35",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "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 { NAV_RUNTIME } from "./nav-runtime.ts";
export { REALTIME_RUNTIME } from "./realtime-runtime.ts"; export { REALTIME_RUNTIME } from "./realtime-runtime.ts";
export { ACTION_RUNTIME } from "./action-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; const CONTROLLER_SECTIONS = ["PRIMARY", "UI", "PIN"] as const;
@@ -73,7 +90,7 @@ const CONTROLLER_LOADER = String.raw`
if (!componentControllerPromise) { if (!componentControllerPromise) {
componentControllerPromise = new Promise(function (resolve, reject) { componentControllerPromise = new Promise(function (resolve, reject) {
var script = document.createElement("script"); var script = document.createElement("script");
script.src = componentControllerUrl; script.src = trustedScriptURL(componentControllerUrl);
script.defer = true; script.defer = true;
script.onload = resolve; script.onload = resolve;
script.onerror = reject; script.onerror = reject;
+128 -12
View File
@@ -26,6 +26,40 @@ export const REACTIVE_RUNTIME = String.raw`
var behaviorObserver; var behaviorObserver;
var clientModuleCache = new Map(); 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 * Two builtin chains the runtime reaches for constantly. Aliasing them is
* not only shorter: hasOwn keeps prototype keys from reading as data, and * 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) { Array.prototype.forEach.call(rules || [], function (rule) {
var cssText = rule.cssText || ""; var cssText = rule.cssText || "";
var match; var match;
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)/g; // Only inspect declarations that can affect the current document. A
while ((match = pattern.exec(cssText))) referenced[match[1]] = true; // 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) { if (rule.style) {
Array.prototype.forEach.call(rule.style, function (property) { Array.prototype.forEach.call(rule.style, function (property) {
if (String(property).indexOf("--wrn-") === 0) declared[property] = true; if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
@@ -980,6 +1027,34 @@ export const REACTIVE_RUNTIME = String.raw`
renderers[index](); renderers[index]();
} }
} }
/*
* Write loop locals onto a client-rendered item, mirroring the
* data-wrn-loop-locals attribute the server emits for an each block.
*
* Client data-for used to pass locals to hydration in memory only, so
* anything that resolves them by READING the DOM -- a component output
* binding calls decodeLoopLocals(componentRoot) -- found nothing and
* silently dropped the value. A plain DOM handler kept working, because it
* receives locals through the hydration closure, which is what made the
* failure look arbitrary. The DOM is the single source of truth for
* locals; both paths write it now.
*/
function writeLoopLocals(node, locals) {
if (!node || node.nodeType !== 1 || !locals) return;
try {
var json = JSON.stringify(locals);
if (json === undefined) return;
node.setAttribute(
"data-wrn-loop-locals",
window.btoa(unescape(encodeURIComponent(json))),
);
} catch (error) {
console.error("[wrnexus] failed to encode loop locals", error);
}
}
function decodeLoopLocals(node) { function decodeLoopLocals(node) {
if (!node) { if (!node) {
return {}; return {};
@@ -1145,6 +1220,7 @@ export const REACTIVE_RUNTIME = String.raw`
* lacks one of these does not break the rest. * lacks one of these does not break the rest.
*/ */
if (name === "toast") return toastApi; if (name === "toast") return toastApi;
if (name === "useFetch") return useFetch;
if (boundWindowGlobals[name] && typeof window[name] === "function") { if (boundWindowGlobals[name] && typeof window[name] === "function") {
return window[name].bind(window); return window[name].bind(window);
} }
@@ -1789,7 +1865,7 @@ export const REACTIVE_RUNTIME = String.raw`
var next = markup == null ? "" : String(markup); var next = markup == null ? "" : String(markup);
if (htmlNode.innerHTML !== next) { if (htmlNode.innerHTML !== next) {
htmlNode.innerHTML = next; htmlNode.innerHTML = trustedHTML(next);
} }
}); });
runHtml(); runHtml();
@@ -1973,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
} }
function clearControlContent() { function clearControlContent() {
if (!rangeEnd) { block.innerHTML = ""; return; } if (!rangeEnd) { block.innerHTML = trustedHTML(""); return; }
while (block.nextSibling && block.nextSibling !== rangeEnd) { while (block.nextSibling && block.nextSibling !== rangeEnd) {
block.parentNode.removeChild(block.nextSibling); block.parentNode.removeChild(block.nextSibling);
} }
@@ -1981,7 +2057,7 @@ export const REACTIVE_RUNTIME = String.raw`
function appendControlContent(markup, locals) { function appendControlContent(markup, locals) {
var template = document.createElement("template"); var template = document.createElement("template");
template.innerHTML = markup || ""; template.innerHTML = trustedHTML(markup || "");
var fragment = template.content; var fragment = template.content;
var elements = toArray(fragment.childNodes).filter(function (node) { var elements = toArray(fragment.childNodes).filter(function (node) {
return node.nodeType === 1; return node.nodeType === 1;
@@ -2216,6 +2292,8 @@ export const REACTIVE_RUNTIME = String.raw`
itemIndex; itemIndex;
} }
writeLoopLocals(clone, locals);
hydrateItem( hydrateItem(
clone, clone,
locals, locals,
@@ -2292,6 +2370,7 @@ export const REACTIVE_RUNTIME = String.raw`
} }
var keyedClone = template.cloneNode(true); var keyedClone = template.cloneNode(true);
writeLoopLocals(keyedClone, keyedLocals);
hydrateItem(keyedClone, keyedLocals); hydrateItem(keyedClone, keyedLocals);
record = { record = {
node: keyedClone, node: keyedClone,
@@ -2367,7 +2446,7 @@ export const REACTIVE_RUNTIME = String.raw`
var markup = value == null ? "" : String(value); var markup = value == null ? "" : String(value);
if (node.innerHTML !== markup) { if (node.innerHTML !== markup) {
node.innerHTML = markup; node.innerHTML = trustedHTML(markup);
} }
}); });
}); });
@@ -2724,9 +2803,17 @@ export const REACTIVE_RUNTIME = String.raw`
); );
} }
}; };
node.addEventListener(outName, domListener); /*
* focus and blur do not bubble, so a bubble-phase listener on the
* component root never hears its own input or button take focus --
* the binding silently did nothing. Capture reaches the descendant.
* The same flag must be passed to removeEventListener or the
* listener outlives the component.
*/
var capture = outName === "focus" || outName === "blur";
node.addEventListener(outName, domListener, capture);
cleanupCallbacks.push(function () { cleanupCallbacks.push(function () {
node.removeEventListener(outName, domListener); node.removeEventListener(outName, domListener, capture);
}); });
}); });
}); });
@@ -2735,9 +2822,15 @@ export const REACTIVE_RUNTIME = String.raw`
// server forwards these markers onto the rendered child root; evaluate // server forwards these markers onto the rendered child root; evaluate
// them here and write changes into the child's prop signals. // them here and write changes into the child's prop signals.
toArray(el.querySelectorAll("*")) 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) { .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) { toArray(node.attributes).forEach(function (attr) {
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return; if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
var binding = pairBinding(attr.value); var binding = pairBinding(attr.value);
@@ -2756,9 +2849,9 @@ export const REACTIVE_RUNTIME = String.raw`
}); });
} catch (_) { return; } } catch (_) { return; }
var apply = function () { 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); else queueMicrotask(apply);
}); });
}); });
@@ -4311,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; window.__wrnexusCallApi = wrnexusCallApi;
function dispatchComponentEvent(root, name, detail) { 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 () => { test("a non-GET request carries the CSRF token from the cookie", async () => {
const { callApi, calls, win } = harness({ status: 200, payload: {} }); const { callApi, calls, win } = harness({ status: 200, payload: {} });
win.document.cookie = "wrn-csrf=token-123"; 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" });
});
+92
View File
@@ -0,0 +1,92 @@
import { test, expect } from "bun:test";
import { Window } from "happy-dom";
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
import { restoreGlobalsAfterAll } from "./global-restore.ts";
restoreGlobalsAfterAll([
"window",
"document",
"location",
"NodeFilter",
"MutationObserver",
"CustomEvent",
]);
function mount(html: string): Window {
const win = new Window() as unknown as Window & Record<string, unknown>;
win.document.body.innerHTML = `<div id="app">${html}</div>`;
const g = globalThis as Record<string, unknown>;
g.window = win;
g.document = win.document;
g.location = win.location;
g.NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter;
g.MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver;
g.CustomEvent = (win as unknown as { CustomEvent: unknown }).CustomEvent;
(0, eval)(REACTIVE_RUNTIME);
return win;
}
// A client-rendered `data-for` passed its loop locals to hydration in memory but
// never wrote the `data-wrn-loop-locals` marker the SSR path writes. Anything
// that resolves locals by READING the DOM -- notably a component's
// `data-wrn-out-*` output binding, which calls decodeLoopLocals(componentRoot)
// -- therefore found nothing and silently dropped the value, with no console
// error. A plain DOM `@click` kept working, because it receives locals through
// the hydration closure instead, which is what made this look arbitrary.
test("a client-rendered for-loop item carries its loop locals in the DOM", () => {
const win = mount(
`<div data-scope="items: [{code: 'small'}, {code: 'large'}]">
<div data-for="item in items" data-component="Card" data-wrn-out-action="pick(item.code)"></div>
</div>`,
);
const mounts = win.document.querySelectorAll("[data-component='Card']");
expect(mounts.length).toBe(2);
const decoded = Array.from(mounts).map((node) => {
const raw = (node as unknown as Element).getAttribute("data-wrn-loop-locals");
expect(raw).toBeTruthy();
return JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
});
// The marker must carry the real item, so an output binding naming `item`
// resolves it rather than silently dropping the call.
expect(decoded[0].item).toEqual({ code: "small" });
expect(decoded[1].item).toEqual({ code: "large" });
});
// The keyed path builds its clones separately, so it needs its own coverage --
// a fix applied to only one of the two loop paths leaves half the bug in place.
test("a keyed for-loop item carries its loop locals too", () => {
const win = mount(
`<div data-scope="rows: [{id: 1, code: 'a'}, {id: 2, code: 'b'}]">
<div data-for="row in rows key row.id" data-component="Card" data-wrn-out-action="pick(row.code)"></div>
</div>`,
);
const decoded = Array.from(win.document.querySelectorAll("[data-component='Card']"), (node) => {
const raw = (node as unknown as Element).getAttribute("data-wrn-loop-locals");
expect(raw).toBeTruthy();
return JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
});
expect(decoded.map((d) => d.row.code)).toEqual(["a", "b"]);
});
// The encoder goes through UTF-8 before base64, as the server's does. btoa on a
// raw JS string throws on any character above U+00FF, which would take out the
// whole loop for an ordinary non-ASCII label.
test("loop locals survive non-ASCII values", () => {
const win = mount(
`<div data-scope="rows: [{label: 'Ünïcode — 日本語'}]">
<div data-for="row in rows" data-component="Card" data-wrn-out-action="pick(row.label)"></div>
</div>`,
);
const raw = win.document
.querySelector("[data-component='Card']")!
.getAttribute("data-wrn-loop-locals");
expect(raw).toBeTruthy();
const decoded = JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
expect(decoded.row.label).toBe("Ünïcode — 日本語");
});
+109
View File
@@ -62,6 +62,13 @@ test("split runtime hydrates a controller only from the controller asset", () =>
expect(win.document.activeElement).toBe(buttons[1]); 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", () => { test("a page without controller markers does not request the controller asset", () => {
const win = mount( const win = mount(
`<main data-scope="count: 1"><span>{count}</span></main>`, `<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"); 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)", () => { test("independent signals in one scope update correctly (dependency tracking)", () => {
const win = mount( const win = mount(
`<div data-scope="a: 0, b: 100"> `<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); 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", () => { test("production runtime strips development diagnostics", () => {
const production = getReactiveRuntime(); const production = getReactiveRuntime();
expect(production).not.toContain("WRN-DEV-"); expect(production).not.toContain("WRN-DEV-");
@@ -1781,3 +1836,57 @@ test("a class binding inside data-for follows state the row never mentions", ()
expect(items()[0]?.classList.contains("is-active")).toBe(false); expect(items()[0]?.classList.contains("is-active")).toBe(false);
expect(items()[1]?.classList.contains("is-active")).toBe(true); expect(items()[1]?.classList.contains("is-active")).toBe(true);
}); });
test("a @focus binding on a component tag fires for a focusable element inside it", () => {
/*
* focus and blur do NOT bubble. The runtime bound every output-named DOM
* fallback listener in the bubble phase, so a parent writing @focus on a
* component tag never heard the component's own input or button take focus:
* the event fired on the descendant and stopped there. Nothing errored --
* the binding simply did nothing, which is why the library still declares
* focus/blur outputs on components that could never deliver them
* (button, TextLink, WysiwygEditor).
*
* The ui ratchet for "outputs nothing emits" excludes natively-named
* outputs on the stated grounds that a native event reaches the root
* anyway. That reasoning holds for click and change, which bubble, and
* fails for focus and blur, which do not.
*/
const win = mount(
`<div data-scope="seen: ''">` +
`<span id="out">{seen}</span>` +
`<div data-scope="n: 0" data-wrn-hydration="Field:x">` +
`<div data-wrn-events="focus" data-wrn-out-focus="seen = 'focused'">` +
`<button id="inner">press</button>` +
`</div></div></div>`,
);
const inner = win.document.querySelector("#inner") as unknown as {
dispatchEvent: (event: unknown) => boolean;
};
// A real focus event: fires on the descendant, does not bubble.
const FocusEventCtor = (win as unknown as { Event: typeof Event }).Event;
inner.dispatchEvent(new FocusEventCtor("focus", { bubbles: false }));
expect(win.document.querySelector("#out")?.textContent).toBe("focused");
});
test("a @change binding still fires for a bubbling event inside a component", () => {
// The companion case: change DOES bubble, and must keep working unchanged.
const win = mount(
`<div data-scope="seen: ''">` +
`<span id="out">{seen}</span>` +
`<div data-scope="n: 0" data-wrn-hydration="Field:y">` +
`<div data-wrn-events="change" data-wrn-out-change="seen = 'changed'">` +
`<input id="inner" />` +
`</div></div></div>`,
);
const inner = win.document.querySelector("#inner") as unknown as {
dispatchEvent: (event: unknown) => boolean;
};
const EventCtor = (win as unknown as { Event: typeof Event }).Event;
inner.dispatchEvent(new EventCtor("change", { bubbles: true }));
expect(win.document.querySelector("#out")?.textContent).toBe("changed");
});
+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. 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. `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", "name": "@wrnexus/db",
"version": "0.8.16", "version": "0.8.20",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "./src/index.ts", "main": "./src/index.ts",
+10 -1
View File
@@ -29,8 +29,17 @@ export interface ModelRef {
model: Model; model: Model;
} }
/** Parse annotated queries from one `.sql` file's contents. */ /**
* Parse annotated queries from one `.sql` file's contents.
*
* Line endings are normalised to LF first. The generator embeds each query's
* SQL as a string literal, so without this a CRLF checkout regenerated every
* query with CRLF where the committed file had LF -- a build dirtied the
* working tree, and that churn buried real changes in the same file. Line
* endings carry no meaning in SQL.
*/
export function parseQueries(content: string): QueryDef[] { export function parseQueries(content: string): QueryDef[] {
content = content.replace(/\r\n?/g, "\n");
const out: QueryDef[] = []; const out: QueryDef[] = [];
const re = /--\s*name:\s*(\w+)\s*:(one|many|exec)\b[^\n]*\n([\s\S]*?)(?=--\s*name:|$)/gi; const re = /--\s*name:\s*(\w+)\s*:(one|many|exec)\b[^\n]*\n([\s\S]*?)(?=--\s*name:|$)/gi;
let m: RegExpExecArray | null; let m: RegExpExecArray | null;
+28
View File
@@ -63,6 +63,34 @@ export function withTransaction<T>(db: Db, callback: (tx: Db) => Promise<T>): Pr
return db.tx(callback); return db.tx(callback);
} }
/** Execute a parameterized compare-and-set update and report whether this caller won. */
export async function compareAndSet(
db: Db,
options: {
table: string;
idColumn?: string;
id: string | number;
stateColumn?: string;
from: string;
to: string;
extra?: Record<string, unknown>;
},
): Promise<boolean> {
const table = identifier(options.table);
const idColumn = identifier(options.idColumn ?? "id");
const stateColumn = identifier(options.stateColumn ?? "status");
const entries = Object.entries(options.extra ?? {});
const assignments = [stateColumn, ...entries.map(([name]) => identifier(name))];
const dialect = db.driver.dialect;
const values = [options.to, ...entries.map(([, value]) => value), options.id, options.from];
const sql = `UPDATE ${table} SET ${assignments
.map((name, index) => `${name} = ${placeholder(dialect, index + 1)}`)
.join(
", ",
)} WHERE ${idColumn} = ${placeholder(dialect, assignments.length + 1)} AND ${stateColumn} = ${placeholder(dialect, assignments.length + 2)}`;
return (await db.exec(sql, values)).changes === 1;
}
/** Conservative default classifier for deadlock/serialization retry errors. */ /** Conservative default classifier for deadlock/serialization retry errors. */
export function isRetryableTransactionError(error: unknown): boolean { export function isRetryableTransactionError(error: unknown): boolean {
if (!error || typeof error !== "object") return false; if (!error || typeof error !== "object") return false;
+20
View File
@@ -54,9 +54,29 @@ export {
exists, exists,
countRows, countRows,
withTransaction, withTransaction,
compareAndSet,
retryTransaction, retryTransaction,
batch, batch,
databaseHealth, databaseHealth,
createRepository, createRepository,
} from "./helpers.ts"; } from "./helpers.ts";
export type { Repository } 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;
}

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