Compare commits

..
Author SHA1 Message Date
Clintchiz 120889060a fix(ui): normalize form field label sizing
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) In progress
2026-08-25 15:25:24 +05:30
Clintchiz 26ec18c24b chore(editor): release vscode extension 0.8.11
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
2026-08-25 15:22:15 +05:30
Clintchiz 7658c5d499 fix(editor): type nested attribute expressions
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-25 15:16:38 +05:30
Clintchiz 3cb6ba1233 fix(ui): render navbar brand prop outside slot
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m32s
2026-08-25 14:00:46 +05:30
Clintchiz 64db7da08d fix(ui): refine responsive catalog navigation
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
2026-08-25 13:46:19 +05:30
Clintchiz 9f6552ea62 fix(ui): separate catalog icon masks from tiles
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-24 23:30:33 +05:30
Clintchiz 5bc391e74f fix(ui): inherit catalog icon contrast
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 22s
2026-08-24 23:23:43 +05:30
Clintchiz 3327ec0d37 refactor(ui): clarify catalog navigation hierarchy
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m18s
2026-08-24 23:16:22 +05:30
Clintchiz ceb865f4f1 fix(ui): prevent navigation surface overflow
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
2026-08-24 23:08:00 +05:30
Clintchiz 45309b3f72 refactor(ui): compact mega menu layouts
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
2026-08-24 23:06:01 +05:30
Clintchiz 8f3a13c854 fix(ui): render only supplied mega menu metadata
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
2026-08-24 22:57:13 +05:30
Clintchiz eca4447dcf fix(ui): stabilize catalog mega menu interaction
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-24 22:50:41 +05:30
Clintchiz 904bca21b2 refactor(ui): refine catalog mega menu navigation
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m52s
2026-08-24 22:40:58 +05:30
Clintchiz b98d417185 fix(ui): eliminate mega menu flash and lag
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
2026-08-24 22:33:02 +05:30
Clintchiz 7d737ec0a0 fix(ui): make catalog mega menus interactive
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 6m15s
2026-08-24 22:18:30 +05:30
Clintchiz f5538ed621 fix(ui): keep mega menu open across hover gap
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
2026-08-24 22:08:46 +05:30
Clintchiz d2d830e7e2 fix(ui): pin navbar mega panels to viewport
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 12m5s
2026-08-24 21:56:27 +05:30
Clintchiz 0618782355 fix(ui): make mega menus work under strict CSP
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
2026-08-24 21:52:25 +05:30
Clintchiz 9067e68f69 fix(ui): import navbar mega menu dependency
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m39s
2026-08-24 21:37:26 +05:30
Clintchiz 9d64ec60c4 feat(ui): add typed navbar menu items
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m12s
2026-08-24 21:36:46 +05:30
Clintchiz fc1eea3939 chore(editor): refresh language server bundle hash
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m8s
2026-08-24 21:25:53 +05:30
Clintchiz 4ec5dad60a fix(formatter): expand large structured prop defaults
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 21s
2026-08-24 21:25:07 +05:30
Clintchiz a1a2947910 feat(ui): expand mega menu layout system
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m2s
2026-08-24 21:15:50 +05:30
Clintchiz 270040fb5f feat(ui): add in-flow floating navbar
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
2026-08-24 19:47:47 +05:30
Clintchiz f9e285b182 feat(ui): own responsive marketing layout APIs
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m42s
2026-08-24 19:44:26 +05:30
Clintchiz 3b402b4e36 fix(ui): collapse empty navbar topbar and add CTA spacing
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 11m48s
2026-08-24 18:28:39 +05:30
Clintchiz fa535575c5 fix(cli): resolve package updates on Windows
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m56s
2026-08-24 17:23:35 +05:30
Clintchiz 46ef4b775b fix(ui): harden public page components
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 10m19s
2026-08-24 17:13:14 +05:30
Clintchiz 7a36c1905f fix(cli): handle version flags before workspace discovery
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
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 (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m52s
2026-08-24 12:05:20 +05:30
Clintchiz 613ff7ae5b fix: invoke npm reliably from Bun on Windows
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
2026-08-24 11:37:06 +05:30
Clintchiz e372ae571a chore: harden release checks and package coverage
Quality / quality (windows-latest) (push) Waiting to run
Quality / quality (ubuntu-latest) (push) Failing after 9m57s
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
Clintchiz cd0dffa87d feat: complete SSR CRM and refine auth UI
Quality / quality (ubuntu-latest) (push) Failing after 11m17s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-20 16:17:59 +05:30
ClintchizandClaude Opus 5 f57bd05a03 feat(language-server): complete and describe api block calls
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:35:31 +05:30
ClintchizandClaude Opus 5 2bb52487eb feat(editor): complete the apis block and drop the removed snippets
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:31:23 +05:30
ClintchizandClaude Opus 5 990a8128a7 feat(editor): highlight the apis block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:31:19 +05:30
ClintchizandClaude Opus 5 7a1b4e5b33 fix(cli): guard the one migration write path that skips parse validation
mode-functions writes without a parse check when a mode wrapper survives
holding api entries, since that intermediate state is unparseable until
move-api-blocks runs later in the same pass. Brace balance is the invariant
a bad splice offset would break, so check that instead; nothing downstream
could tell a corrupted wrapper from an untouched one.

Also records that Task 5's example-app migration ran against an already-
migrated target and so did not prove end-to-end behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:24:19 +05:30
ClintchizandClaude Opus 5 aded2daab9 fix: restore the production gate after the migration tasks
- rebuild editors/vscode bundles, stale since the parser escape fix
- attach the caught ParseError as `cause` in both migration validators
- drop two unused test bindings flagged by eslint

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:19:53 +05:30
ClintchizandClaude Opus 5 6bb3ab5fe7 feat(cli): fail the update when a project needs manual review
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:12:38 +05:30
ClintchizandClaude Opus 5 4aa0973352 feat(cli): report legacy api bodies for manual migration
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 12:03:17 +05:30
ClintchizandClaude Opus 5 e616ed276e feat(cli): migrate mode-scoped helpers to shared functions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:58:16 +05:30
ClintchizandClaude Opus 5 74490964ee feat(cli): migrate api entries into the apis block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:51:44 +05:30
ClintchizandClaude Opus 5 7a3e55b150 feat(cli): migrate away the dead config keys
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:45:40 +05:30
ClintchizandClaude Opus 5 fb24cc7ec3 fix(syntax): stop swallowing literal backslashes in attribute values
readQuoted treated \X as an escape for any X, so a single literal
backslash in any quoted attribute value was silently dropped
(data-path="C:\Users" parsed as C:Users) and a doubled backslash
collapsed to one. Only the delimiter and the backslash itself are
escapes now; every other backslash is a literal character.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:42:29 +05:30
Clintchiz 63316111cb feat(examples): worked example for apis blocks 2026-08-20 08:36:03 +05:30
ClintchizandClaude Opus 5 0ed8351828 test: restore executed and real-tsc coverage lost when the old api-block tests were deleted
Fix round 1: the deleted api-block-*.test.ts files were not fully superseded
by the apis-* siblings as claimed. Ports back, using apis {} fixtures:
- brace-inside-a-string-literal response-section scanner regression test
- type erasure of response/error bodies before browser emission
- client-side response-error-not-swallowed / transport-failure-fallback,
  executed via dynamic import of a generated browser module
- the full SSR execution suite: response payload binding, error section
  status/message/data binding, {#each} failure propagation, all executed
  via dynamic import + a real load/api call chain (not string checks)
- the four real-tsc enforcement tests (matching/wrong-type/extra-field/
  missing-field), plus the B1 cross-page collision guard and the B6
  export-for-noUnusedLocals guard

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 08:14:16 +05:30
ClintchizandClaude Opus 5 890d6106b3 feat: replace the ssr/client data blocks with apis blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:59:37 +05:30
ClintchizandClaude Opus 5 de99a2c2e0 feat(cli): assert types for every api block with declared fields
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:44:58 +05:30
ClintchizandClaude Opus 5 442c058d0c feat(compiler): support the three api render-binding forms
- Parse api="name", api="name()", and api="name({ ... })" render bindings
  for apis {} (mode "any") blocks, mirroring @click="fn()" syntax.
- Render-bind by calling Task 4's generated server `api` object directly
  (api.<name>(args)) rather than re-implementing the fetch/response
  transport, spliced into the SSR template via the existing loop/expression
  sentinel mechanism so the call runs inside the async render function with
  await support.
- A block that is both render-bound and called from code runs twice by
  design (no dedup); pinned with a test.
- Fix packages/syntax's attribute-value lexer (readQuoted) to honor
  backslash-escaped quotes, needed so an api="..." call expression can
  itself contain a quoted string/object literal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 07:38:32 +05:30
Clintchiz a08dfa5322 fix(compiler): reject dynamic api access in client functions
Usage-driven emission can only see api.<name> calls. api["name"]()
or passing api to a helper is invisible to it, silently drops the
block from the browser bundle, and fails at runtime instead of build
time. Detect that dynamic/indirect use (masking strings and comments
first, reusing the tokenizer's skipLiteralOrComment) and refuse to
compile instead, naming the offending function.
2026-08-20 07:25:02 +05:30
Clintchiz 712a6d3d8c feat(compiler): emit browser api bindings only where the client calls them 2026-08-20 07:19:55 +05:30
Clintchiz 9dec811069 fix(compiler): route the generated api object through the real buildApiRequest
Root-cause fix for the fix-round-1 review: the inlined query/body
assembly in __wrnexusCallApi was a third, unguarded copy of
buildApiRequest's rules. Restore the import of buildApiRequest from
@wrnexus/core in the generated module and delete the inline copy.

The four api-block-ssr.test.ts tests (and three in compiler.test.ts)
that dynamically import a generated module from an OS tmpdir were
failing against a stale globally-installed @wrnexus/core (v0.8.8,
predates buildApiRequest) because that tmpdir has no node_modules of
its own and bare-specifier resolution walked out of the workspace.
Fixed at the source: symlink the workspace @wrnexus/core into each
tmpdir root before the dynamic import, the same way every in-repo
package already resolves it.
2026-08-20 07:11:53 +05:30
Clintchiz 847b7010d1 feat(compiler): emit the server-side api object
Server module now declares `const api = { ... }` for apis {} blocks in
mode "any", dispatching in-process via requireRequestContext + the
existing __wrnexusCallApi transport helper. The try wraps only the
transport call; the response body runs after it, outside the try, so
a bug in the author's response code surfaces rather than being
mistaken for a request failure. A block with no error {} section
rethrows instead of resolving undefined.

Also closes the pageCtx.__wrnexusCallApi wiring gap in
dev-server/runtime.ts: it now forwards input through to
callApiFromContext instead of dropping it.
2026-08-20 07:03:11 +05:30
ClintchizandClaude Opus 5 1dbe16dc93 test(core,csr): replace text-substring agreement check with a behavioural one
The old assertions only searched REACTIVE_RUNTIME for substrings; they never
touched buildApiRequest and were not anchored to the content-type line they
claimed to guard, so they could not detect drift on either side. Replace
with a fixture-driven test that runs both implementations on the same
(path, method, input) cases and compares the actual request they produce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 06:47:20 +05:30
ClintchizandClaude Opus 5 f32b33e3b6 feat(core): share API request assembly between both transports
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 06:43:42 +05:30
Clintchiz 768074ac0a fix(dev-server): route server.fn() RPC to a handler in production
server.fn() posts to POST /__wrnexus/rpc. Dev intercepts that path before
handlers.fetch and routes it to a createRpcHandler instance built from
loadWrnServerModule; createProductionServer/createProductionHandlers had no
such route, so the request fell through to the internal-caller-gated
inter-app service RPC and 404'd.

Add resolveProdServerFunctions(), a synchronous equivalent of dev's resolve
that searches the already-statically-imported ProdManifest components/pages/
layouts for __wrnexusServerFunctions + __wrnexusRpcManifest, and wire it into
createProductionHandlers with the same validateCsrf + withServerFnRequestContext
wrapping dev uses. Move those two helpers into a new rpc-shared.ts so prod.ts
can use them without a circular import through index.ts.

Add packages/dev-server/test/prod-server-fn-rpc.test.ts covering a successful
call, CSRF rejection, and clean 404s for an unknown component/function.
2026-08-20 06:41:32 +05:30
ClintchizandClaude Opus 5 c7e40154ca fix(core): establish request context at every server-code entry point
Wraps the three additional entry points where user server code runs
outside fetchHandler's own context wrap:
- the server-function RPC path (/__wrnexus/rpc) intercepted before
  handlers.fetch in the dev server (index.ts) - what server.fn() travels
- the service RPC path (isRpcPath) inside fetchHandler, which runs
  implement()/implementStream() service code before ctx existed
- the HMR-sync handler, which runs real load blocks/actions via dispatch()

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:55:18 +05:30
ClintchizandClaude Opus 5 680ea73975 feat(core): carry the request context in an AsyncLocalStorage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:46:24 +05:30
ClintchizandClaude Opus 5 953b1cd692 fix(syntax): reject bare apis-container bodies and cross-mode duplicate api names
Bare bodies inside apis {} silently discarded their text with no error,
producing a do-nothing block. They now throw a ParseError naming the entry
and pointing at the response {} section. Duplicate-name detection for
dataApis moved from an incremental, order-dependent check (only saw prior
entries in the array) to a single post-parse pass over the whole ast.dataApis,
so it catches cross-mode duplicates (apis {} vs ssr { api }) regardless of
declaration order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:33:05 +05:30
ClintchizandClaude Opus 5 87a00de5f3 feat(syntax): parse the apis container block
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:28:23 +05:30
ClintchizandClaude Opus 5 d069f5ddd7 fix: correct feature-report prose, restore update test coverage, warn on sub-0.8.0 upgrades
- generate-complete-framework-report.mjs no longer claims legacyEmit/
  legacyEventProps/legacyComponentDiscovery/stringLayouts/
  functions.legacyDefaultRuntime are usable compatibility flags; they were
  removed before the first public release and a config setting them is now
  rejected. Regenerated docs/WRNEXUS-COMPLETE-FEATURE-REPORT.md.
- Restored the update.test.ts coverage lost in the sub-0.8.0 migration
  cleanup: a 0.8.x fixture now asserts refreshFrameworkFiles' still-live
  behaviour (public/llms.txt and CLAUDE.md creation) and that
  pkg.wrnexus.version stays at its old value after an unverified update.
  The .gitignore refresh and build/start/production script backfill were
  themselves removed as part of dropping the sub-0.8.0 migrations that
  implemented them, so there is nothing left to cover for those two.
- updateApp now logs a clear warning when the detected project version is
  below 0.8.0, naming the version and stating that automated migration from
  below 0.8.0 is no longer supported, without failing the command (a
  marker-less app, like examples/basic-app, is benign and must still
  upgrade cleanly).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:19:23 +05:30
ClintchizandClaude Opus 5 abebbcf8af chore: regenerate baselines after the legacy cleanup
- regenerate docs/public-api-0.8.json (removals only: CompatibilityPolicy,
  CompatibilityReport, CURRENT_COMPATIBILITY_DATE, CURRENT_FRAMEWORK_BEHAVIOUR,
  isCompatibilityDate, resolveCompatibility from @wrnexus/styles)
- regenerate docs/WRNEXUS-COMPLETE-FEATURE-REPORT.md
- remove the deleted 'wrnexus compatibility' command row and its config-pinning
  sentence from packages/cli/README.md
- drop compatibilityDate/frameworkBehaviour example fields from docs/GUIDE.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 02:04:59 +05:30
ClintchizandClaude Opus 5 5720f93db1 test(auth): restore plugin engine-hook coverage via invokeAuthHandler
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:43:38 +05:30
ClintchizandClaude Opus 5 7f5e1bc3cf refactor: remove the deprecated auth options
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:38:08 +05:30
ClintchizandClaude Opus 5 97d60d0ba8 refactor: drop the deprecated compiler re-export shims
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:33:48 +05:30
ClintchizandClaude Opus 5 e7e7b58160 chore: drop update migrations below 0.8.0
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:28:33 +05:30
ClintchizandClaude Opus 5 e09a35b4bd fix(store): give the fix-round-2 regression test explicit generics
The single-branch 'shared'-only action fixture failed to infer through
defineStore's actions generic, typing store.increment as never and
failing bun run typecheck (TS2349) even though bun test passed.
Explicit type arguments fix the inference without weakening the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 01:15:35 +05:30
Clintchiz 5c8f3ede54 refactor: delete the compatibility config surface 2026-08-20 01:11:35 +05:30
ClintchizandClaude Opus 5 d19595c7ba fix(store): drop unreachable legacy StoreRuntime fallback
Fix round 2 for task 1: store-codegen.ts stopped emitting runtime:
"legacy" actions in round 1, making the StoreRuntime variant and its
resolution fallback dead. Narrows StoreRuntime to three variants and
adds a regression test for the remaining options.runtime -> shared
fallback chain.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:58:26 +05:30
ClintchizandClaude Opus 5 b7796b103a fix(typecheck): drop legacy FunctionRuntime branches in contracts/index
Fix round 1 for task 1: packages/typecheck also branched on the
removed legacy runtime (componentContract's exclusion filter and the
runtime-namespace loop). Removes both, adds a regression test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 00:51:21 +05:30
ClintchizandClaude Opus 5 ec63090006 refactor: replace the legacy function runtime with shared
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:13:05 +05:30
ClintchizandClaude Opus 5 224af8fd96 docs: four implementation plans for the cleanup, apis block, migration, and editor work
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 23:05:31 +05:30
ClintchizandClaude Opus 5 68cc75d0b0 docs: fold the auth deprecations into the cleanup spec as a firm scope
They are our own superseded options, not a stale dependency. The recommended
form is already what the showcase example uses; the blast radius is three
test files inside packages/auth, and the rpId/origin options are already
ignored at runtime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:55:50 +05:30
ClintchizandClaude Opus 5 43652c14af docs: specs for apis blocks, migration, and editor tooling
Three specs completing the set, each depending on the one before it:

- apis {}: one container, mode-less declarations, api.<name>() callable
  anywhere with build-time dispatch, AsyncLocalStorage for server context,
  usage-driven emission, three render-binding forms. Replaces the ssr {} /
  client {} data blocks and the untypeable with($data) legacy body.
- update: migrations to the new syntax. The legacy bare-body rewrite is
  deliberately manual -- which free identifiers are payload fields is not
  knowable from the source, so an automatic guess would compile and be wrong.
- editor tooling: grammar, completions for api. and the api= attribute,
  diagnostics for removed constructs, and resolving by observation whether
  generated type errors surface inline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:52:31 +05:30
ClintchizandClaude Opus 5 e40d8319a6 docs: spec for the legacy, deprecated, and unused-config cleanup
All seven compatibility keys are dead configuration: traced every reference
and none is read by any compiler, codegen, or runtime code. They are written
into every generated config and ignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 22:47:22 +05:30
ClintchizandClaude Opus 5 2f075df42c release: patch cli, compiler, core, csr, syntax, validation
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
Typed, callable api blocks for .wrn files: sectioned request/response/error,
callable from client code as api.name(input), type-checked by tsc against the
route contracts.

Also two fixes found along the way: defineEndpoint never received its input
through the real router, so every typed endpoint was validating undefined; and
v.boolean() silently coerced any unrecognised string to false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:36:44 +05:30
ClintchizandClaude Opus 5 281615a4b0 fix(validation): stop unrecognised boolean strings coercing to a silent false
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
checkField in packages/validation/src/index.ts (and its browser mirror in
runtime.ts) treated any string other than "true"/"on" as false with no
error, so typos like "treu" or values like "yes"/"1"/"TRUE" silently
passed as false.

Now:
- true/false booleans pass through unchanged
- recognised true strings (case-insensitive, trimmed): true, on, 1, yes
- recognised false strings: false, off, 0, no
- numeric 1/0 coerce (JSON payloads)
- undefined/null/"" still coerce to false (unchecked-checkbox semantics)
- anything else is now a type error (desc.typeMessage or "Must be true or
  false") instead of a silent false

Locked-in behaviours preserved: a required boolean given false still
errors, and parseEnv DEBUG: "true" coercion still works.

Added coverage for recognised strings, numeric 1/0, the type-error
regression guard, absent/empty handling, the required+false case, and a
client/server parity test driving both checkField and the browser runtime
through the same inputs.

Blast radius: searched packages/, examples/, services/ for v.boolean()
usage; all existing call sites (auth consent/rememberDevice, db 'active'
default, example consent checkboxes) feed true/false/'on'/absent values,
none of which change behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 21:26:20 +05:30
Clintchiz 3ae5d7cf97 fix(compiler): stop error {} from swallowing response {} bugs in api blocks
Client codegen chained .then().catch(), so a .catch() after .then()
caught exceptions thrown by the response body too. Switched to the
two-argument then(onFulfilled, onRejected) form, whose rejection
handler cannot see errors from the fulfilment handler.

SSR codegen wrapped both the transport call and the response-body eval
in the same try; only __wrnexusCallApi is now inside the try, and
__wrnexusEvalData runs after it, outside.

Also verifies (and locks in with a regression test) that GET query
numbers already coerce correctly through defineEndpoint + checkField,
and documents that in the typed-api-block spec.
2026-08-19 21:11:26 +05:30
ClintchizandClaude Opus 5 b5029889a5 fix: address all seven final-gate findings for typed api blocks
B1: qualify each generated __wrn_api_check_* assertion name with a short
hash of the page's path (relative to app/, for reproducibility across
checkouts) so two pages declaring a same-named block no longer collide
with an identical type alias (TS2300).

B2: skip assertion emission for any block that is not client-mode, or
that has zero declared request fields. ssr sectioned blocks can never
declare a request and always fell back to Record<string, never>, whose
keyof is `string` -- making the key-exactness arm of AssertAssignable
evaluate to false unconditionally (TS2344) on every ssr sectioned block
regardless of correctness. Chose to skip both non-client blocks and
zero-field client blocks, since neither has anything meaningful to
assert type-safety about.

B3: only resolve the endpoint's input (query params / ctx.req.json())
when the endpoint declares an input schema. Previously the router-set
fix accidentally read the request body unconditionally, so a handler
with no input schema that parses the request itself hit
ERR_BODY_ALREADY_USED.

B4: run response/error bodies in client-mode api blocks through
eraseFunctionTypes, matching every other browser-bound body in
client-codegen.ts, so a TypeScript-only construct inside one (e.g. an
annotated locally-declared function) doesn't reach the .mjs artifact.

B5: only exclude "api" from state/prop destructuring in the generated
browser module when the page actually has client-mode api blocks (i.e.
there is a real `api` binding to shadow). Previously "api" was always
excluded, so a page with `state api` and no api blocks got an
undeclared `api` reference (ReferenceError) in client code.

B6: prefix each emitted assertion with `export`, so it isn't flagged as
an unused local under a downstream project's noUnusedLocals (TS6196).

B7: wrnexusCallApi now resolves with undefined for an ok 204/205
response, or an ok response with an empty/unparseable body, instead of
rejecting with "Response was not valid JSON" -- matching the spec's
failure table (error path only for non-2xx, network failure, or an
actually unparseable body on a non-empty response).

Regenerated examples/basic-app's generated types and editor bundles to
match. Confirmed the example's type gate still fails when an
unaccepted field is added to a request body, and passes cleanly
otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:38:22 +05:30
ClintchizandClaude Opus 5 e9db4ca24d test(core): cover defineEndpoint's no-second-argument request-parsing path
Every existing test in endpoint-schema.test.ts passed rawInput
explicitly, so the branch added to endpoint.ts's fix (GET/HEAD query
parsing, JSON body parsing, malformed/absent body fallback) was
exercised by nothing but a manual curl. Add coverage that calls the
endpoint with only a context, matching the real router's calling
convention:

- GET with query parameters populates input from ctx.url.searchParams.
- POST with a JSON body populates input from the parsed body.
- POST with a malformed or absent body does not throw; the schema's
  own validation decides the outcome (asserted on the real response).
- An explicit rawInput argument still wins and the request is never
  read (the body is drained first, so a second .json() call would
  reject if the endpoint tried to read it again) -- the regression
  guard for the branch intentionally left untouched.

Confirmed the GET and POST-body tests fail against the pre-fix
endpoint.ts (input resolves as undefined/null instead of the sent
value); the malformed/absent-body test does not distinguish pre- and
post-fix, because in that specific edge case both normalize to an
effectively empty input -- noted in the report rather than forced.

Added a CHANGELOG entry documenting the behavior change for
downstream apps: a request that previously passed vacuous validation
on a defineEndpoint route can now legitimately fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:16:32 +05:30
ClintchizandClaude Opus 5 5318320c70 fix(examples): make the api-block demo endpoint actually type-check
directory.ts previously exported a plain (ctx: Context) => ... handler.
With that shape ApiInput<> resolved to unknown, so the generated
__wrn_api_check assertion for the demo page passed trivially even with
a field the endpoint does not accept -- the worked example did not
demonstrate the type safety it exists to demonstrate.

Rewrite directory.ts to use defineEndpoint with a schema (matching
typed-user.ts), which gives the generated assertion a real input type
to check against. Confirmed: adding an unaccepted field to the block's
request body now fails typecheck naming
__wrn_api_check_searchDirectory; removing it passes with zero net
diff.

Fix a real bug this surfaced: packages/core/src/endpoint.ts only read
its input from a second 'rawInput' argument, but the actual HTTP
router (packages/dev-server/src/runtime.ts handleApi) invokes route
handlers as handler(ctx) with no second argument. Every
defineEndpoint-based route -- including the pre-existing typed-user.ts
example -- silently received an empty/undefined input through the
real router (confirmed via curl: valid typed-user payloads were
rejected as 'Required'; directory's name filter matched every record
regardless of query). Fixed by having the endpoint wrapper parse the
request itself (query params for GET/HEAD, JSON body otherwise) when
no rawInput is explicitly supplied, while still honoring an explicit
rawInput for direct/unit-test callers.

Also update api-block-demo.wrn's response section: defineEndpoint
wraps handler output as { data: ... }, so the block's raw response
body is now { data: { users: [...] } } -- response reads
data.data.users instead of data.users.

Re-verified in a real browser after the endpoint rewrite and the
router fix: search returns exactly "Ajay, Asha", exactly one
POST /api/directory carrying x-csrf-token, and the error section
still runs cleanly (no exception, empty result) on a missing route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 20:05:10 +05:30
Clintchiz b3ed9689fa docs: add complete framework feature report
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-19 17:52:08 +05:30
ClintchizandClaude Opus 5 e5de7b54a5 feat(examples): worked example for typed api blocks
- Add examples/basic-app/app/api/directory.ts and app/pages/api-block-demo.wrn
  as the end-to-end worked example for typed api blocks (Task 6).
- Add **/*.generated.api-checks.ts to .prettierignore: this generated file
  must match the CLI's raw output byte-for-byte for check:generated-types,
  and prettier was reformatting it.
- Fix packages/csr/test/api-call.test.ts: no-unsafe-function-type lint error
  from the raw Function type, uncovered while running the full gate.
- Rebuild editors/vscode bundles (packages/compiler and packages/syntax
  changed in Tasks 1-5).
- Regenerate docs/public-api-0.8.json (additive: isIdentPart, isIdentStart,
  skipLiteralOrComment newly exported from packages/syntax).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:08:49 +05:30
ClintchizandClaude Opus 5 3252b1b20e fix(compiler): type-check the emitted ssr binding array and share error handling across call sites
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:42:38 +05:30
ClintchizandClaude Opus 5 7601477f7d feat(compiler): run error section on ssr api call failure
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:24:20 +05:30
ClintchizandClaude Opus 5 847b6dbe59 feat(compiler): support sections in ssr api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:19:27 +05:30
ClintchizandClaude Opus 5 04be24ddd8 fix(cli): close the extra-field gap in AssertAssignable, add tsc-based enforcement tests
AssertAssignable was one-directional ([Actual] extends [Expected]), so a
block declaring a field the contract doesn't accept passed silently
(TypeScript's excess-property check only applies to fresh object
literals, not conditional-type extends). Add a key-exactness check
(Exclude<keyof Actual, keyof Expected> extends never) alongside the
assignability check. Guard it with 'unknown extends Expected' so
untyped (no defineEndpoint contract) routes still only warn, per the
existing behaviour, instead of being forced to fail on every declared
field.

Add packages/cli/test/api-block-types.test.ts cases that regenerate a
fixture and run the real TypeScript compiler (via bunx tsc) over the
generated output, asserting on its diagnostics rather than on emitted
text: matching fields compile clean; a wrong-typed field, an extra
field (the Finding-A regression guard), and a missing required field
all fail, each pointing at the offending block's __wrn_api_check_*
line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:14:13 +05:30
ClintchizandClaude Opus 5 785e8a65bd fix(cli): move api-block assertions into a real .ts file
skipLibCheck exempts .d.ts contents from being checked, so assertions
written inside wrnexus.generated.d.ts were never evaluated by tsc.
Emit them into wrnexus.generated.api-checks.ts instead, referencing
the WRNexusGenerated namespace's helper types (which stay in the
.d.ts). Track the new generated file in check:generated-types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 16:01:07 +05:30
ClintchizandClaude Opus 5 a2698fb51a docs: correct the api block spec's type-enforcement mechanism
Assertions in a .d.ts are inert under skipLibCheck: true, which the root
tsconfig sets. Proven during implementation by forcing skipLibCheck: false,
where the same assertion fires as TS2344. They move to a generated .ts file,
which skipLibCheck does not exempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:58:05 +05:30
ClintchizandClaude Opus 5 419614d9d1 feat(cli): generate type assertions for api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:56:02 +05:30
ClintchizandClaude Opus 5 70777e4a45 fix(compiler): reserve api as a runtime binding to avoid client-scope collision
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:45:09 +05:30
ClintchizandClaude Opus 5 bd2f6ac5e3 feat(compiler): compile client api blocks into the browser module
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:41:13 +05:30
Clintchiz b457ad1d54 feat(csr): add the api block transport 2026-08-19 15:37:23 +05:30
ClintchizandClaude Opus 5 323f57b32b fix(syntax): make api-section scanning string/comment-aware
Reuse tokenizer.readBalancedBraces string/comment skipping (extracted as
skipLiteralOrComment) for both section detection and slicing, instead of a
second hand-rolled brace counter. Fixes truncation on braces inside strings
and false-positive sectioned detection from keywords inside comments/strings.
Also switch api-sections.ts errors from plain Error to LexError so parser.ts
upgrades them to ParseError with an offset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:30:51 +05:30
ClintchizandClaude Opus 5 028c2a6d64 feat(syntax): parse sectioned api blocks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:21:10 +05:30
ClintchizandClaude Opus 5 2f0f82b29f chore: ignore the subagent-driven-development workspace
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:18:39 +05:30
ClintchizandClaude Opus 5 50097ec4b4 docs: implementation plan for typed api blocks
Six tasks: parse the sections, add the CSR transport, compile client-mode
blocks into the browser module, generate the tsc assertions, support
sections in ssr blocks, and verify end to end in a browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:15:02 +05:30
ClintchizandClaude Opus 5 f026415ba6 docs: design for typed, callable api blocks in .wrn files
A sectioned `api` block -- request / response / error -- callable on
demand from client code, typed against the API route contracts the types
generator already emits.

Records the constraints that shaped it: the current block cannot carry a
query string (isSafeApiPath rejects "?"), cannot interpolate (readPath
stops at "{"), has nowhere to put a body, and fetches once. And the one
that decides the type-safety mechanism -- generated build artifacts are
not type-checked, so enforcement goes into the generated .d.ts, which the
project's own tsc already compiles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:09:30 +05:30
ClintchizandClaude Opus 5 2cbc3e43e1 release: patch compiler, cli, dev-server
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
Strips TypeScript from client function bodies when emitting browser
modules, so `wrnexus build` no longer fails on an annotated local.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:17:51 +05:30
ClintchizandClaude Opus 5 55fed2177a fix(compiler): strip TypeScript from client function bodies
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
`wrnexus build` failed on any client function whose body used TypeScript:

    const requestBody: Record<string, unknown> = {}
    error: Expected ";" but found ":"

Codegen copies a client function's body into the browser module verbatim.
It removes the types from the function's *signature*, which is what made
this easy to miss -- the emitted module looked transpiled, and only bodies
carried types through. The artifact is written as .mjs and read back as
plain JavaScript, so the failure surfaced as a syntax error in generated
code rather than at the .wrn line responsible.

Browser modules are now transpiled before they are written, at all three
sites that emit one (the production build and both dev-server paths).

Reproduced end to end: a page with an annotated body failed the build with
the reported errors, and after the fix builds, ships valid minified JS, and
runs -- the handler sets its state correctly in a browser.

Note: the same body is also embedded as a string for the CSP-safe fallback
interpreter, which still receives it untranspiled. The compiled module
shadows the fallback, so this is only reachable in the window before that
module loads. Left alone here because stripping it lives in codegen, which
also runs under Node in the editor bundle where the Bun transpiler is
unavailable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 14:03:14 +05:30
ClintchizandClaude Opus 5 8c609edd32 release: patch csr, dev-server, language-server, react, ui
Quality / quality (ubuntu-latest) (push) Failing after 11m36s
Quality / quality (windows-latest) (push) Canceled after 0s
Client control blocks and loops, the dev-server rebuild recycle, the
editor's tag and completion handling, and the island mount/HMR fixes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:42:26 +05:30
ClintchizandClaude Opus 5 e898929193 fix(react): mount visible islands and load rebuilt code after HMR
Quality / quality (ubuntu-latest) (push) Failing after 9m55s
Quality / quality (windows-latest) (push) Canceled after 0s
Two faults found by driving the island demo in a real browser. Both were
silent: the markup, every asset, and all 48 island tests were correct
either way.

An island renders nothing until it mounts, so its placeholder is
zero-height, and IntersectionObserver does not treat a zero-area target
consistently -- client:visible islands mounted on one load and not the
next. Visibility for those is now decided from the element's own rect,
driven by scroll and resize; a placeholder with real size still uses the
observer. The strategy had no test at all, which is why this shipped.

After an island source edit the browser kept running the old code. The
rebuild worked and the file was refetched, but the loader imports a URL
that does not change, and the browser caches modules by URL. Remounts now
carry a generation the dev loader folds into the request.

Verified in the browser: mounts with start={3} as a number, clicks reach
React (3 -> 5), and an edit to Counter.tsx now shows the new text and
stays interactive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 13:32:05 +05:30
ClintchizandClaude Opus 5 18a1c40118 fix(dev-server): recycle the server once hot rebuilds pile up
The dev server got slower the longer it ran. Measured on the example app:
30 .wrn edits grew RSS from 117 MB to 137 MB and never gave it back, while
30 CSS edits cost nothing -- so the leak is exactly one retained module
identity per rebuild, not caches or file handles.

That is inherent to reloading a module in-process. Bun caches modules by
path, so a rebuild has to be given a new identity to be picked up at all,
and Bun has no API to unload the old one. At roughly 0.66 MB a rebuild, a
long editing session is several hundred megabytes of garbage that cannot
be collected.

The process now recycles itself past a rebuild threshold, exiting with the
RESTART_EXIT_CODE the CLI supervisor already respawns on; browsers
reconnect because the HMR client already retries. It waits for a quiet
period first so a live request is never cut off, and the threshold (300
rebuilds, about 200 MB) sits well above a normal session. Set
WRNEXUS_DEV_RECYCLE_AFTER to tune it, or 0 to switch it off.

Also bounds browserArtifactPaths and islandArtifactPaths, which are keyed
by content hash and so gained an entry per rebuild that was never read
again. Small next to the module leak, but unbounded is unbounded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:30:54 +05:30
ClintchizandClaude Opus 5 a20f143acb fix: isolate test globals, close tags at every caret, trim the runtime
Three pre-existing issues that the previous commit worked around rather
than solved.

Test global pollution. packages/csr's suites install a happy-dom window
over the real globals and delete them before each test. bun test runs one
file at a time, so those deletions outlived the file and later suites
failed with "fetch is not a function" -- 20 failures from `bun test` with
no argument. They now restore what they captured. The editor's Node tests
shim the vscode host by patching Module._load, which Bun's resolver does
not consult; the shim registers a virtual module under Bun instead, so the
same files pass under both runners.

Multi-cursor tag auto-close. The handler now closes the tag at every
caret. Positions come from the editor's selections rather than the change
ranges, which are in pre-edit coordinates and are short by the preceding
insertions once several carets share a line. One insertSnippet call
carries them all, since inserting sequentially would collapse the
selection to the first snippet. Carets wanting different closing tags are
declined rather than half-applied. Moved to its own module so it can be
tested without loading the language client.

Runtime size. Trimmed 2,414 bytes: the global lookup tables became one
prototype-safe scheme (a name like "toString" was previously a hit on
Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the
repeated chains, and dead code went. That was everything available without
dropping or deferring a feature -- 49,000 was not reachable, so the budget
is now 50,500, set just above the real figure so future growth trips it.

Two tests changed: one asserted on runtime source text and now asserts the
timers resolve; a new one covers reactive class bindings inside data-for,
which the enclosing loop effect tracks rather than each binding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:14:18 +05:30
ClintchizandClaude Opus 5 ac248f2bb0 fix(csr): run for/while loops and keep declarations out of state
The client runtime had no loop support, so any shared function using one
returned early -- Pagination and ButtonGroup were broken client-side, not
just in tests.

Adding loops exposed two further faults:

- A var reaching writeScope creates a signal and triggers a render sweep.
  A declaration inside a function called during a render therefore looped
  forever. Declarations now bind into the handler locals instead.
- A control block removed from the DOM keeps its effect in the renderers
  list. Running it against a detached node threw, aborting the sweep and
  leaving every later effect stale.

Also raises the reactive runtime budget to 53,000: the runtime had already
grown past 49,000 before this change, and 52,570 minified is 16,803 gzipped.

Two deferred minors: html-service leaves absent documentation undefined
rather than an empty string, and the extension declines tag auto-close on
multi-cursor edits rather than closing only the first cursor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 02:24:41 +05:30
ClintchizandClaude Opus 5 0904a4efaa fix(csr): render control blocks created by a client rerender
{#if}, {#each} and their {:else}/{:else if}/{:empty} branches worked on
the server and after hydration, but a block nested inside another block
stayed empty once the outer block rerendered. Adding a row to a list
produced the row's markup with its inner block markers in place and
nothing between them, for the life of the page.

Two causes, both on the client-created path only:

reactive() registers an effect; effects run when renderAll sweeps the
list. A state change runs just the affected effects rather than sweeping,
so an effect registered during that rerender was queued and never
invoked. setupControlBlock now returns its runner and the creating block
invokes it immediately.

The first reactive pass is skipped so hydration does not discard
server-rendered DOM. A block created by a rerender has no server DOM, so
skipping its only pass left it permanently empty. firstRun is now keyed
off outerLocals, which is set only on the client-created path.

Verified in a browser as well as in tests: adding a group to a list now
renders the new row's nested {:else}, and the existing rows' nested loops
survive the rerender.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 00:49:31 +05:30
Clintchiz 5b11b937bb Release CLI with reactive control block runtime
Quality / quality (ubuntu-latest) (push) Failing after 9m51s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-19 00:27:32 +05:30
Clintchiz f199385204 Make if and each blocks reactive on the client
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-19 00:22:00 +05:30
Clintchiz c0c2fa4595 chore(release): publish CLI 0.8.43
Quality / quality (ubuntu-latest) (push) Failing after 9m48s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 23:38:54 +05:30
Clintchiz cfdcdd00ad chore(release): publish dev server 0.8.39
Quality / quality (ubuntu-latest) (push) Failing after 9m58s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 23:31:21 +05:30
Clintchiz 03d5cb6aa6 fix(gateway): proxy browser server functions to workspace apps
Quality / quality (ubuntu-latest) (push) Failing after 10m40s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 23:30:42 +05:30
Clintchiz 701acd828c fix(vscode): avoid relative-link parsing in changelog
Quality / quality (ubuntu-latest) (push) Failing after 9m54s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:55:11 +05:30
Clintchiz b28b79c370 fix(vscode): use supported Marketplace publish flags
Quality / quality (ubuntu-latest) (push) Failing after 6m1s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:52:59 +05:30
Clintchiz fdd0c9f847 chore: complete HTML editing verification
Quality / quality (ubuntu-latest) (push) Failing after 10m13s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:49:07 +05:30
Clintchiz 7ad336b4dd chore(vscode): prepare 0.8.8 marketplace release
Quality / quality (ubuntu-latest) (push) Failing after 9m53s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:43:01 +05:30
Clintchiz d70e89230b chore(release): publish language server 0.8.10
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-18 22:38:47 +05:30
ClintchizandClaude Opus 5 7e6d8c3bc8 test(language-server): add end-to-end verification for HTML editing support
Adds a scratch page (kept intentionally, per controller ruling on Task 8)
and one end-to-end test that drives the real language server over LSP
stdio against that page's real text, verifying completion (with the
seo-block negative case tested via a simulated '<' keystroke and
mutation-verified against html-regions.ts), hover, folding ranges,
linked editing, and wrn/tagComplete all work together on realistic
content.

Regenerates routes.gen.ts and wrnexus.generated.d.ts for the new page's
route, required by check:generated-types.

Two checks from the original brief (auto-close-tag insertion and Emmet
Tab-expansion) require a live VS Code Extension Development Host and
are documented as outstanding manual verification in
.superpowers/sdd/2026-08-18-wrn-html-editing/task-8-report.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:59:19 +05:30
ClintchizandClaude Opus 5 92c0920c9b test(vscode): guard the Emmet mapping and auto-close setting
Also fix an unused-var lint failure in completion-scope.test.js blocking the production gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:36:20 +05:30
Clintchiz a8d8ac386f test(vscode): add integration tests for completion provider guard 2026-08-18 21:28:38 +05:30
Clintchiz 2c8841cc5f fix(vscode): stop duplicating completions inside view blocks 2026-08-18 21:23:54 +05:30
ClintchizandClaude Opus 5 b344d2a70a fix(vscode): guard tag auto-close against replaced selections and stale round-trips
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:20:16 +05:30
ClintchizandClaude Opus 5 e83f0366ef feat(vscode): close HTML tags as they are typed in .wrn files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:16:47 +05:30
Clintchiz d9e8f5be82 feat(language-server): add tag folding and linked editing 2026-08-18 21:10:57 +05:30
Clintchiz bd0c1317ff test(language-server): cover didClose region-cache clear end-to-end
Replaces the direct-call-only test with an over-stdio test that exercises
server.ts's didClose handler itself, so it fails if the
clearHtmlRegionCache wiring is removed or misparameterized.
2026-08-18 21:04:33 +05:30
Clintchiz 97801287f9 feat(language-server): merge HTML completions and hover into one response 2026-08-18 20:56:17 +05:30
Clintchiz d77638131b fix(language-server): don't self-close tags inside quoted attribute values 2026-08-18 20:47:19 +05:30
Clintchiz 609224591c feat(language-server): answer HTML completion, hover, folding, and tag close 2026-08-18 20:43:47 +05:30
Clintchiz 6074d19c43 fix(language-server): bypass cache for version-less documents 2026-08-18 20:38:37 +05:30
Clintchiz 7301849a7f feat(language-server): add offset-preserving virtual HTML document 2026-08-18 20:34:24 +05:30
ClintchizandClaude Opus 5 7d481df652 docs(html-editing): add implementation plan
Eight TDD tasks: the view-block scanner and virtual document, the HTML
service wrapper, merging HTML into completion and hover, folding and
linked editing, auto-close on type, standing down the duplicate client
provider, manifest guards, and a manual editor check.

Task 1 comes first because everything reads positions through it: its
length-and-newline invariant is what removes position mapping, and a
break there would misreport positions everywhere rather than fail.

The last task is manual verification in an Extension Development Host.
Unit tests cannot show that completions actually appear in an editor, and
a green suite has hidden non-functional features in this repo before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:30:34 +05:30
ClintchizandClaude Opus 5 5477f5436d docs(html-editing): add design spec for HTML support in .wrn files
Markup in a .wrn file highlights but has no tag or attribute completion,
no tag closing, and no tag-level folding: the grammar's embeddedLanguages
mapping only affects tokenization, and VS Code's HTML language service
never runs on these documents.

The design extracts view blocks into a virtual HTML document where
everything outside them is blanked to whitespace of identical length, so
source positions and virtual positions are the same and no mapping table
is needed. Region detection is a tolerant scanner rather than the parser,
because completion fires while the document is mid-edit and unparseable.

Completion merges WRNexus and HTML entries into one list ranked by
sortText, which also fixes an existing bug: the extension and the server
both answer completion on '<' today, so VS Code concatenates two lists.

Two decisions worth review:

- HTML formatting is excluded. formatWrn already formats markup, knows
  WRNexus syntax, and would fight a second formatter that is free to
  rewrite spacing inside @click={...} and client:visible.
- Only auto-close-on-type is client-side. Linked editing is standard LSP
  and lives in the shared server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:23:08 +05:30
ClintchizandClaude Opus 5 b646ec8d00 chore(release): patch-bump packages changed since the last publish
Quality / quality (ubuntu-latest) (push) Failing after 12m48s
Quality / quality (windows-latest) (push) Canceled after 0s
cli 0.8.42, csr 0.8.22, db 0.8.16, dev-server 0.8.38,
dev-toolbar 0.8.13, i18n 0.8.12.

Every previous version was already on the registry, so the HMR client
repair, the i18n JSON data block, the gateway WebSocket origin fix, and
the generated-dialect stamp were not reachable by consumers.

compiler and react are unchanged since their last publish and are not
bumped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 20:01:01 +05:30
ClintchizandClaude Opus 5 e66d2425aa fix(gateway): allow HMR sockets on every configured domain
Quality / quality (ubuntu-latest) (push) Failing after 13m52s
Quality / quality (windows-latest) (push) Canceled after 0s
The WebSocket origin check compared the browser's Origin host, which
carries the port, against configured domains, which do not. publicOrigin
only ever matches domains[0], so every other domain fell through to that
comparison and was denied purely on the port: web.localhost:3000 never
matched web.localhost.

The result was a 403 on the HMR upgrade and a client reconnecting
forever, while the page itself loaded fine because HTTP routing resolves
the Host separately.

Compares hostnames now. Unrelated and lookalike-suffix origins are still
denied, and both cases are covered by tests.

Verified through a real gateway: the HMR socket opens on both localhost
and web.localhost, and a live edit reaches the browser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:53:27 +05:30
ClintchizandClaude Opus 5 b3b65dddd8 fix(db): stamp the dialect into generated query files
Quality / quality (ubuntu-latest) (push) Failing after 12m46s
Quality / quality (windows-latest) (push) Canceled after 0s
The same generate command emitted ? one run and $1 the next, which looked
like non-determinism. It is not: postgres uses $1 placeholders where
sqlite and mysql use ?, and the driver comes from the active profile, so
building under a different profile rewrites this committed file.

The header now records the dialect it was generated for, making the flip
visible in the diff and explaining check:generated-types failures instead
of leaving them looking like random churn.

Worth deciding separately: a committed artifact whose contents depend on
the active profile will keep drifting. Either generate per dialect, or
stop committing it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:44:36 +05:30
ClintchizandClaude Opus 5 5dbcc5b85d fix(i18n): ship i18n data as a JSON block so CSP cannot block it
window.__wrnI18n was undefined in development: the payload shipped as an
executable inline script, and a document's CSP nonce is fixed at load, so
any such script arriving from a later response is blocked. Client
translations and language switching silently had no data.

The payload is now a type="application/json" block, which the browser
never executes and script-src therefore never applies to. The i18n
runtime, CSR navigation, and HMR all read the block instead of matching
window.__wrnI18n= with a regex.

Pages now render zero executable inline scripts, so an inline script-src
violation is structurally impossible rather than merely unobserved. Zero
framework JavaScript on island-free routes is unaffected: the block is
inert data, and nothing loads to read it unless the page needs it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 19:44:28 +05:30
419 changed files with 55187 additions and 3693 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
+3
View File
@@ -25,3 +25,6 @@ tsconfig.focus.json
# "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`, # "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`,
# which requires the scaffold to live inside the repo tree). # which requires the scaffold to live inside the repo tree).
**/test/.tmp-*/ **/test/.tmp-*/
# Subagent-driven-development scratch (ledger, briefs, review packages)
.superpowers/
+1
View File
@@ -13,6 +13,7 @@ bun.lockb
# Generated code (queries.gen.ts, routes.gen.ts, etc.) # Generated code (queries.gen.ts, routes.gen.ts, etc.)
**/*.gen.ts **/*.gen.ts
**/*.generated.d.ts **/*.generated.d.ts
**/*.generated.api-checks.ts
# Bundled .wrn compiler for the VS Code extension (generated) # Bundled .wrn compiler for the VS Code extension (generated)
editors/vscode/src/compiler.cjs editors/vscode/src/compiler.cjs
+39
View File
@@ -1,5 +1,44 @@
# Changelog # Changelog
## 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`
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.
`"yes"`, `"1"`, `"TRUE"`, `"treu"`) passed validation as a silent, wrong `false`. Now
recognised true strings (`"true"`, `"on"`, `"1"`, `"yes"`, case-insensitive and trimmed) and
false strings (`"false"`, `"off"`, `"0"`, `"no"`) coerce as expected, numeric `1`/`0` coerce
(for JSON payloads), and absent/empty input (`undefined`/`null`/`""`) still coerces to
`false` exactly as before (unchanged HTML-checkbox semantics). **Behavior change for
downstream apps:** any other value — an unrecognised string, an object, an array — is now a
type error (`desc.typeMessage` or "Must be true or false") instead of a silent `false`. A
required boolean field given `false` still errors, as before (checkbox-required semantics
are unchanged). A repo-wide search of `packages/`, `examples/`, and `services/` found no
existing `v.boolean()` usage that feeds an unrecognised value, so no call sites are expected
to start failing.
- Fixed `defineEndpoint` (`@wrnexus/core`) so routes invoked through the real HTTP router
(which calls handlers as `handler(ctx)`, with no second argument) actually receive their
request input: it now parses query parameters for GET/HEAD and the JSON body otherwise
when no input is passed explicitly. Previously such endpoints silently validated
`undefined`, so an `input` schema with only optional fields passed vacuously regardless of
what was sent. **Behavior change for downstream apps:** a request that previously passed
vacuous validation on a `defineEndpoint` route can now legitimately fail (400
`VALIDATION_ERROR`) if it does not actually satisfy the schema. Explicitly passing a second
argument (e.g. from a unit test or an internal caller) is unaffected and still takes
priority over reading the request.
## 0.8.8 ## 0.8.8
- Added the framework request context to `.wrn` language-server type environments. - Added the framework request context to `.wrn` language-server type environments.
+178 -111
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",
@@ -93,6 +93,25 @@
"typescript": "^6.0.3", "typescript": "^6.0.3",
}, },
}, },
"examples/crm-app": {
"name": "wrnexus-crm-example",
"version": "0.8.0",
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/styles": "workspace:*",
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.123",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3",
},
},
"examples/i18n-showcase": { "examples/i18n-showcase": {
"name": "i18n-showcase", "name": "i18n-showcase",
"version": "0.8.0", "version": "0.8.0",
@@ -114,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",
@@ -126,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",
@@ -154,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",
@@ -167,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",
@@ -195,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",
@@ -216,7 +235,7 @@
}, },
"packages/auth": { "packages/auth": {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.12", "version": "0.8.18",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/captcha": "workspace:*", "@wrnexus/captcha": "workspace:*",
@@ -238,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": {
@@ -257,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:*",
@@ -272,7 +292,7 @@
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.8.41", "version": "0.8.64",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -300,7 +320,7 @@
}, },
"packages/compiler": { "packages/compiler": {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.8.11", "version": "0.8.22",
"dependencies": { "dependencies": {
"@wrnexus/csr": "workspace:*", "@wrnexus/csr": "workspace:*",
"@wrnexus/store": "workspace:*", "@wrnexus/store": "workspace:*",
@@ -318,18 +338,18 @@
}, },
"packages/core": { "packages/core": {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.8.9", "version": "0.8.12",
}, },
"packages/csr": { "packages/csr": {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.8.21", "version": "0.8.34",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/db": { "packages/db": {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.8.15", "version": "0.8.20",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -337,7 +357,7 @@
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.8.37", "version": "0.8.56",
"dependencies": { "dependencies": {
"@wrnexus/authz": "workspace:*", "@wrnexus/authz": "workspace:*",
"@wrnexus/cache": "workspace:*", "@wrnexus/cache": "workspace:*",
@@ -351,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:*",
@@ -364,7 +386,7 @@
}, },
"packages/dev-toolbar": { "packages/dev-toolbar": {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.8.12", "version": "0.8.16",
"devDependencies": { "devDependencies": {
"@types/bun": "^1.3.14", "@types/bun": "^1.3.14",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -372,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",
@@ -390,14 +413,16 @@
}, },
"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": {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.8.11", "version": "0.8.12",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -451,13 +476,22 @@
}, },
"packages/language-server": { "packages/language-server": {
"name": "@wrnexus/language-server", "name": "@wrnexus/language-server",
"version": "0.8.9", "version": "0.8.12",
"bin": { "bin": {
"wrnexus-language-server": "src/server.ts", "wrnexus-language-server": "src/server.ts",
}, },
"dependencies": { "dependencies": {
"@wrnexus/syntax": "workspace:*", "@wrnexus/syntax": "workspace:*",
"@wrnexus/typecheck": "workspace:*", "@wrnexus/typecheck": "workspace:*",
"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": {
@@ -467,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",
@@ -492,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",
@@ -525,15 +571,16 @@
}, },
"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:*",
}, },
}, },
"packages/react": { "packages/react": {
"name": "@wrnexus/react", "name": "@wrnexus/react",
"version": "0.8.8", "version": "0.8.9",
"dependencies": { "dependencies": {
"@wrnexus/store": "workspace:*", "@wrnexus/store": "workspace:*",
}, },
@@ -552,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",
@@ -570,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:*",
@@ -592,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:*",
}, },
@@ -608,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:*",
@@ -621,11 +668,11 @@
}, },
"packages/syntax": { "packages/syntax": {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.8.9", "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",
@@ -633,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",
@@ -641,14 +688,14 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.8.19", "version": "0.8.45",
"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:*",
@@ -662,7 +709,7 @@
}, },
"packages/validation": { "packages/validation": {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.8.10", "version": "0.8.11",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
"@wrnexus/plugin": "workspace:*", "@wrnexus/plugin": "workspace:*",
@@ -749,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=="],
@@ -775,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=="],
@@ -795,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=="],
@@ -825,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=="],
@@ -929,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=="],
@@ -937,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=="],
@@ -967,6 +1016,8 @@
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.67.0", "", { "dependencies": { "@typescript-eslint/types": "8.67.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA=="],
"@vscode/l10n": ["@vscode/l10n@0.0.18", "", {}, "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ=="],
"@wrnexus/ai": ["@wrnexus/ai@workspace:packages/ai"], "@wrnexus/ai": ["@wrnexus/ai@workspace:packages/ai"],
"@wrnexus/auth": ["@wrnexus/auth@workspace:packages/auth"], "@wrnexus/auth": ["@wrnexus/auth@workspace:packages/auth"],
@@ -1011,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"],
@@ -1023,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"],
@@ -1065,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=="],
@@ -1091,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=="],
@@ -1123,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=="],
@@ -1145,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=="],
@@ -1181,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=="],
@@ -1189,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"],
@@ -1261,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=="],
@@ -1297,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=="],
@@ -1315,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=="],
@@ -1327,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=="],
@@ -1387,6 +1444,14 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vscode-html-languageservice": ["vscode-html-languageservice@5.6.2", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "^3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-ulCrSnFnfQ16YzvwnYUgEbUEl/ZG7u2eV27YhvLObSHKkb8fw1Z9cgsnUwjTEeDIdJDoTDTDpxuhQwoenoLNMg=="],
"vscode-languageserver-textdocument": ["vscode-languageserver-textdocument@1.0.12", "", {}, "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA=="],
"vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"web": ["web@workspace:examples/inter-app-api-showcase/apps/web"], "web": ["web@workspace:examples/inter-app-api-showcase/apps/web"],
"whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="],
@@ -1395,27 +1460,29 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"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=="], "wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
"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=="],
-2
View File
@@ -1288,8 +1288,6 @@ wrnexus help | --help | -h
import type { AppConfig } from "@wrnexus/styles"; import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = { const config: AppConfig = {
compatibilityDate: "2026-08-02",
frameworkBehaviour: 1,
head: [ '<link rel="stylesheet" href="…">' ], // string | string[] → appended to every <head> head: [ '<link rel="stylesheet" href="…">' ], // string | string[] → appended to every <head>
seo: { // SeoConfig (global defaults, merged per page) seo: { // SeoConfig (global defaults, merged per page)
+4
View File
@@ -166,6 +166,10 @@ Supported view features include:
- `{#each items as item, index key item.id}`, optional keys, and optional `{:empty}` branches - `{#each items as item, index key item.id}`, optional keys, and optional `{:empty}` branches
- comments and scoped styles - comments and scoped styles
`{#if}` and `{#each}` are rendered on the server for the initial response and
remain reactive after hydration. Browser state changes switch conditional
branches and rerender loop rows, including the `{:empty}` branch.
Output is escaped by default. Explicit raw HTML APIs must be treated as security Output is escaped by default. Explicit raw HTML APIs must be treated as security
boundaries. boundaries.
File diff suppressed because it is too large Load Diff
+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
+249 -14
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",
@@ -1040,7 +1071,8 @@
"rpcManifest", "rpcManifest",
"runtimeCapabilities", "runtimeCapabilities",
"runtimeTypeOf", "runtimeTypeOf",
"serializeIslandProps" "serializeIslandProps",
"stripBrowserTypes"
] ]
}, },
"@wrnexus/content": { "@wrnexus/content": {
@@ -1083,6 +1115,7 @@
"BackoffStrategy", "BackoffStrategy",
"Bucket", "Bucket",
"BudgetViolation", "BudgetViolation",
"BuiltApiRequest",
"Bulkhead", "Bulkhead",
"BulkheadOptions", "BulkheadOptions",
"CSRF_COOKIE", "CSRF_COOKIE",
@@ -1135,6 +1168,8 @@
"POSTGRES_TENANT_DIRECTORY_SCHEMA", "POSTGRES_TENANT_DIRECTORY_SCHEMA",
"PageComponent", "PageComponent",
"PageMeta", "PageMeta",
"PaginationInput",
"PaginationOptions",
"PerformanceBudgets", "PerformanceBudgets",
"PerformanceMeasurement", "PerformanceMeasurement",
"PermissionsPolicyConfig", "PermissionsPolicyConfig",
@@ -1196,6 +1231,7 @@
"TenantResource", "TenantResource",
"TenantSqlClient", "TenantSqlClient",
"Tracer", "Tracer",
"TransactionCookie",
"TrustedTypesConfig", "TrustedTypesConfig",
"UploadError", "UploadError",
"UploadInspectionResult", "UploadInspectionResult",
@@ -1203,6 +1239,7 @@
"UploadScanner", "UploadScanner",
"assertTenantAccess", "assertTenantAccess",
"bridgeRealtime", "bridgeRealtime",
"buildApiRequest",
"cacheControl", "cacheControl",
"checkPerformanceBudgets", "checkPerformanceBudgets",
"collectUploads", "collectUploads",
@@ -1228,6 +1265,7 @@
"escapeHtml", "escapeHtml",
"etag", "etag",
"executionContextFromHttp", "executionContextFromHttp",
"getRequestContext",
"getUser", "getUser",
"hashPassword", "hashPassword",
"isRoomDefinition", "isRoomDefinition",
@@ -1259,9 +1297,11 @@
"requestId", "requestId",
"requestLogger", "requestLogger",
"requireAuth", "requireAuth",
"requireRequestContext",
"requireTenant", "requireTenant",
"resilientCall", "resilientCall",
"resolveRequestUrl", "resolveRequestUrl",
"runWithRequestContext",
"sanitizeFilename", "sanitizeFilename",
"saveUpload", "saveUpload",
"saveUploadSecure", "saveUploadSecure",
@@ -1313,6 +1353,8 @@
"ActionClientError", "ActionClientError",
"ActionClientOptions", "ActionClientOptions",
"ActionResult", "ActionResult",
"ApiClientOptions",
"ApiRequest",
"ClientModuleScope", "ClientModuleScope",
"HydrationScopeApi", "HydrationScopeApi",
"NAV_RUNTIME", "NAV_RUNTIME",
@@ -1326,6 +1368,7 @@
"callServerFunction", "callServerFunction",
"collectRefs", "collectRefs",
"createActionClient", "createActionClient",
"createApiClient",
"createOutputProxy", "createOutputProxy",
"createServerProxy", "createServerProxy",
"getActionRuntime", "getActionRuntime",
@@ -1336,11 +1379,13 @@
"invalidateClientModule", "invalidateClientModule",
"invokeOutput", "invokeOutput",
"loadClientFunctions", "loadClientFunctions",
"registerOutputHandler" "registerOutputHandler",
"useFetch"
] ]
}, },
"@wrnexus/db": { "@wrnexus/db": {
".": [ ".": [
"AddSeedOptions",
"BaseType", "BaseType",
"Column", "Column",
"ColumnDef", "ColumnDef",
@@ -1351,6 +1396,7 @@
"Dialect", "Dialect",
"Driver", "Driver",
"ExecResult", "ExecResult",
"LedgerOptions",
"Migration", "Migration",
"MigrationRunOptions", "MigrationRunOptions",
"MigrationSafetyIssue", "MigrationSafetyIssue",
@@ -1367,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",
@@ -1381,6 +1434,8 @@
"cursorPaginate", "cursorPaginate",
"databaseHealth", "databaseHealth",
"databaseNames", "databaseNames",
"defineLedger",
"defineSeed",
"exists", "exists",
"firstOrThrow", "firstOrThrow",
"generateQueriesFile", "generateQueriesFile",
@@ -1398,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"
], ],
@@ -1464,6 +1524,7 @@
"startServer", "startServer",
"toRequest", "toRequest",
"validateRpcCsrf", "validateRpcCsrf",
"withServerFnRequestContext",
"writeResponse" "writeResponse"
], ],
"./serve-entry": [] "./serve-entry": []
@@ -1606,6 +1667,7 @@
"EncryptionKey", "EncryptionKey",
"EncryptionKeyring", "EncryptionKeyring",
"ReplayStore", "ReplayStore",
"SecretResourceOptions",
"createEncryptedRequest", "createEncryptedRequest",
"createKeyring", "createKeyring",
"createMemoryReplayStore", "createMemoryReplayStore",
@@ -1613,6 +1675,7 @@
"decryptEncryptedResponse", "decryptEncryptedResponse",
"decryptHttpBody", "decryptHttpBody",
"decryptRequest", "decryptRequest",
"defineSecretResource",
"deriveKey", "deriveKey",
"encrypt", "encrypt",
"encryptHttpBody", "encryptHttpBody",
@@ -1646,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"
@@ -1674,6 +1750,7 @@
"@wrnexus/i18n": { "@wrnexus/i18n": {
".": [ ".": [
"ExtractedTranslationKey", "ExtractedTranslationKey",
"I18N_DATA_ATTRIBUTE",
"I18N_JS_HREF", "I18N_JS_HREF",
"I18N_RUNTIME", "I18N_RUNTIME",
"I18nConfig", "I18nConfig",
@@ -1710,6 +1787,7 @@
"plural", "plural",
"pseudoLocalize", "pseudoLocalize",
"renderI18nData", "renderI18nData",
"renderI18nDataTag",
"resolveI18n", "resolveI18n",
"resolveLang", "resolveLang",
"translateHtml", "translateHtml",
@@ -1831,7 +1909,7 @@
"Position", "Position",
"Range", "Range",
"TextDocument", "TextDocument",
"WRN_COMPLETIONS", "WRN_KEYWORDS",
"WorkspaceCompletionItem", "WorkspaceCompletionItem",
"clearWorkspaceIndexCache", "clearWorkspaceIndexCache",
"completionItems", "completionItems",
@@ -1852,7 +1930,31 @@
"workspaceCompletionItems", "workspaceCompletionItems",
"workspaceSymbolLocations" "workspaceSymbolLocations"
], ],
"./server": [] "./server": [
"ApiCallCompletionItem",
"apiCallCompletions",
"apiCallHover",
"isApiReferenceAt"
]
},
"@wrnexus/mail": {
".": [
"MailDriver",
"MailMessage",
"MailOptions",
"MailSandboxDecision",
"MailTemplate",
"SealedMailCredentialsOptions",
"SealedMailDriverOptions",
"defineMail",
"defineMailTemplate",
"defineSealedMailCredentials",
"interpolateTemplate",
"mailSandboxDecision",
"queuedMail",
"sealedMailDriver",
"storedMailTemplate"
]
}, },
"@wrnexus/mcp": { "@wrnexus/mcp": {
".": [ ".": [
@@ -1865,6 +1967,20 @@
"runMcpStdio" "runMcpStdio"
] ]
}, },
"@wrnexus/metering": {
".": [
"EntitlementPlan",
"EntitlementsOptions",
"MeterKind",
"MeterOptions",
"MeterPack",
"MeterResult",
"MeterStore",
"defineEntitlements",
"defineMeter",
"definePacks"
]
},
"@wrnexus/mobile": { "@wrnexus/mobile": {
".": [ ".": [
"CapacitorBridge", "CapacitorBridge",
@@ -2065,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",
@@ -2229,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",
@@ -2254,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": {
@@ -2312,6 +2537,7 @@
".": [ ".": [
"AnimationTimeline", "AnimationTimeline",
"Cleanup", "Cleanup",
"FormState",
"HistorySignal", "HistorySignal",
"ReactiveContext", "ReactiveContext",
"ReactiveScope", "ReactiveScope",
@@ -2338,6 +2564,7 @@
"transition", "transition",
"untrack", "untrack",
"urlSignal", "urlSignal",
"useForm",
"watch" "watch"
] ]
}, },
@@ -2625,15 +2852,12 @@
".": [ ".": [
"ACCENT_COOKIE", "ACCENT_COOKIE",
"AppConfig", "AppConfig",
"AppConfigInput",
"BrowserCookieApi", "BrowserCookieApi",
"BrowserCookieOptions", "BrowserCookieOptions",
"BrowserCookiePreference", "BrowserCookiePreference",
"BrowserCookiesConfig", "BrowserCookiesConfig",
"BuildConfig", "BuildConfig",
"CURRENT_COMPATIBILITY_DATE",
"CURRENT_FRAMEWORK_BEHAVIOUR",
"CompatibilityPolicy",
"CompatibilityReport",
"ConfigIssue", "ConfigIssue",
"ContrastResult", "ContrastResult",
"CssPerformanceAuditIssue", "CssPerformanceAuditIssue",
@@ -2653,6 +2877,7 @@
"ObservabilityConfig", "ObservabilityConfig",
"PerformanceConfig", "PerformanceConfig",
"PwaConfig", "PwaConfig",
"ResolvedAppConfig",
"ResolvedConfigLayers", "ResolvedConfigLayers",
"ResolvedTheme", "ResolvedTheme",
"StyleProcessContext", "StyleProcessContext",
@@ -2684,7 +2909,6 @@
"findStyleEntry", "findStyleEntry",
"fontCspSources", "fontCspSources",
"headToString", "headToString",
"isCompatibilityDate",
"loadAppConfig", "loadAppConfig",
"loadEnv", "loadEnv",
"loadRawConfig", "loadRawConfig",
@@ -2697,7 +2921,6 @@
"renderThemeRuntime", "renderThemeRuntime",
"resolveAccentName", "resolveAccentName",
"resolveBrowserCookieOptions", "resolveBrowserCookieOptions",
"resolveCompatibility",
"resolveConfigLayers", "resolveConfigLayers",
"resolveProfile", "resolveProfile",
"resolveThemeConfig", "resolveThemeConfig",
@@ -2785,6 +3008,7 @@
"parseStructuredImports", "parseStructuredImports",
"positionAt", "positionAt",
"runtimeTypeOf", "runtimeTypeOf",
"skipLiteralOrComment",
"sliceSource", "sliceSource",
"stripRuntimeFunctionModifiers", "stripRuntimeFunctionModifiers",
"supportsSyntaxFeature", "supportsSyntaxFeature",
@@ -2851,7 +3075,10 @@
"LexError", "LexError",
"Lexer", "Lexer",
"Token", "Token",
"TokenType" "TokenType",
"isIdentPart",
"isIdentStart",
"skipLiteralOrComment"
], ],
"./types": [ "./types": [
"RuntimeType", "RuntimeType",
@@ -2865,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",
@@ -2880,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",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,400 @@
# Editor Tooling Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** The language server and VS Code extension understand `apis { }`, complete `api.<name>()`, flag the removed constructs, and stop offering syntax the compiler rejects.
**Architecture:** Four surfaces change independently — the TextMate grammar, the keyword and snippet completions, the server's call completion and hover, and diagnostics for removed constructs. A final task answers by observation whether generated type errors surface inside `.wrn`, which has never been verified.
**Tech Stack:** Bun, TypeScript, `bun:test`, `node --test`, LSP, TextMate grammars.
**Spec:** `docs/superpowers/specs/2026-08-19-editor-tooling-design.md`
## Global Constraints
- `client` is one word with several jobs: `client state { }`, `runtime = "client"`, and the `client function` modifier all survive. **Only the `client { }` / `ssr { }` data-block patterns are removed.** Blanket removal would un-highlight constructs that still exist.
- Offering a construct the compiler rejects is worse than offering nothing.
- **Nothing may claim inline diagnostics work until someone has seen them work.**
- The editor bundles embed the compiler and language server — rebuild with `bun run --cwd editors/vscode build`, or the `check:editor-*` gates fail on a stale bundle.
- `bun run format` before every commit; the gate is `bun run check:production`.
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
---
### Task 1: Grammar
**Files:**
- Modify: `editors/vscode/syntaxes/wrn.tmLanguage.json`
- Test: `editors/vscode/test/grammar-apis.test.js`
**Interfaces:**
- Produces: `apis` highlights as a block keyword; entries highlight as declarations.
- [ ] **Step 1: Write the failing test**
Create `editors/vscode/test/grammar-apis.test.js`:
```js
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const grammar = readFileSync(join(__dirname, "../syntaxes/wrn.tmLanguage.json"), "utf8");
test("the grammar knows the apis block", () => {
assert.ok(grammar.includes("apis"), "apis should appear as a block keyword");
});
test("client keeps its highlighting where it is still valid", () => {
// client state {}, runtime = "client", and client function all survive.
// Only the client {} data block was removed.
assert.ok(grammar.includes("client"), "client must still be matched");
assert.ok(grammar.includes("shared"), "the shared function modifier must still be matched");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `node --test editors/vscode/test/grammar-apis.test.js`
Expected: FAIL on the first assertion — `apis` is absent.
- [ ] **Step 3: Add `apis`, remove only the data-block patterns**
Add `apis` to the block-keyword pattern alongside `functions`. Then find the patterns matching `ssr`/`client` as **data blocks** and remove only those. Leave every rule that matches `client` in `client state`, in `runtime` values, and as a function modifier.
Add a pattern for an entry — `<name> <METHOD> <path>` — so a declaration reads as a declaration.
- [ ] **Step 4: Run the test and check by eye**
Run: `node --test editors/vscode/test/grammar-apis.test.js`
Expected: PASS. Then open a `.wrn` file using `apis { }`, `client state { }`, `functions { shared function }`, and `runtime = "client"` in VS Code and confirm each still colours correctly. Record what you saw.
- [ ] **Step 5: Commit**
```bash
bun run format
git add editors/vscode
git commit -m "feat(editor): highlight the apis block"
```
---
### Task 2: Keyword and snippet completion
**Files:**
- Modify: `packages/language-server/src/index.ts` (`WRN_KEYWORDS`, ~line 31)
- Modify: `editors/vscode/src/completion.js` (block snippets)
- Test: `packages/language-server/test/apis-completion.test.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: `apis` is a known keyword; `ssr` / `client` data-block snippets are gone.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/apis-completion.test.ts`:
```ts
import { expect, test } from "bun:test";
import { WRN_KEYWORDS } from "../src/index.ts";
test("apis is a known page-level keyword", () => {
expect(WRN_KEYWORDS).toContain("apis");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/apis-completion.test.ts`
Expected: FAIL — `apis` is missing.
- [ ] **Step 3: Add the keyword and the snippets**
Add `"apis"` to `WRN_KEYWORDS`. In `editors/vscode/src/completion.js`, add a container snippet and an entry snippet including the `request` / `response` / `error` sections, and remove any `ssr {` / `client {` data-block snippet.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/language-server && bun run --cwd editors/vscode test`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/language-server editors/vscode
git commit -m "feat(editor): complete the apis block and drop the removed snippets"
```
---
### Task 3: `api.` call completion and hover
**Files:**
- Modify: `packages/language-server/src/server.ts` (completion and hover handlers)
- Test: `packages/language-server/test/api-call-completion.test.ts`
**Interfaces:**
- Consumes: `ast.dataApis` entries, which carry `name`, `method`, `path`, and `sections`.
- Produces: completion items for `api.` and hover detail for a block name.
This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor should know it.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/api-call-completion.test.ts`:
```ts
import { expect, test } from "bun:test";
import { apiCallCompletions, apiCallHover } from "../src/server.ts";
const SOURCE = `page Search {
apis {
searchUsers POST /api/users {
request { body { name?: string } }
response { return data.users }
}
listTeams GET /api/teams {
response { return data.teams }
}
}
functions {
client async function go(): Promise<void> {
await api.
}
}
view { <main>x</main> }
}
`;
test("api. offers every declared block with method and path", () => {
const items = apiCallCompletions(SOURCE);
const labels = items.map((item) => item.label);
expect(labels).toContain("searchUsers");
expect(labels).toContain("listTeams");
const search = items.find((item) => item.label === "searchUsers")!;
expect(search.detail).toContain("POST");
expect(search.detail).toContain("/api/users");
});
test("hovering a block name reports its method, path and request fields", () => {
const hover = apiCallHover(SOURCE, "searchUsers");
expect(hover).toContain("POST");
expect(hover).toContain("/api/users");
expect(hover).toContain("name");
});
test("a page with no apis block offers nothing", () => {
expect(apiCallCompletions(`page P { view { <main>x</main> } }`)).toEqual([]);
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/api-call-completion.test.ts`
Expected: FAIL — the functions do not exist.
- [ ] **Step 3: Implement and export both functions**
Add `apiCallCompletions(source: string)` and `apiCallHover(source: string, name: string)` to `packages/language-server/src/server.ts`, parsing with `@wrnexus/syntax` and reading `ast.dataApis`. Wire `apiCallCompletions` into the `textDocument/completion` handler for positions immediately after `api.`, and `apiCallHover` into `textDocument/hover`.
The parser must tolerate the half-typed `await api.` in the fixture. If it throws, fall back to returning `[]` rather than failing the request — completion fires while the document does not parse, which is the normal case.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/language-server`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/language-server
git commit -m "feat(language-server): complete and describe api block calls"
```
---
### Task 4: The `api=` attribute in markup
**Files:**
- Modify: `packages/language-server/src/html-service.ts`
- Test: `packages/language-server/test/api-attribute.test.ts`
**Interfaces:**
- Consumes: `apiCallCompletions` from Task 3.
- Produces: `api="…"` is not flagged as unknown, and completion inside the quotes offers block names.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/api-attribute.test.ts` asserting that (a) an `api="searchUsers"` attribute produces no unknown-attribute diagnostic, and (b) completion inside the quotes offers `searchUsers`. Reuse the fixture shape from Task 3.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/api-attribute.test.ts`
Expected: FAIL.
- [ ] **Step 3: Teach the HTML service about the attribute**
Treat `api` as a known attribute on any element, and route completion inside its quotes to `apiCallCompletions`. The value is a call expression, not text — it must not be spell-checked or reformatted as prose.
- [ ] **Step 4: Run the tests and commit**
```bash
bun test packages/language-server
bun run format
git add packages/language-server
git commit -m "feat(language-server): understand the api binding attribute"
```
---
### Task 5: Diagnostics for the removed constructs
**Files:**
- Modify: `packages/language-server/src/diagnostics` entry point (wherever `.wrn` diagnostics are produced)
- Test: `packages/language-server/test/removed-construct-diagnostics.test.ts`
**Interfaces:**
- Produces: an `ssr { api … }` or `client { api … }` block yields a diagnostic naming `apis { }`, positioned on the block keyword.
The compiler already rejects these. The editor should say so while typing, and say what to do instead.
- [ ] **Step 1: Write the failing test**
Create `packages/language-server/test/removed-construct-diagnostics.test.ts`:
```ts
import { expect, test } from "bun:test";
import { diagnoseWrn } from "../src/index.ts";
test("an ssr data block is flagged and names the replacement", () => {
const diagnostics = diagnoseWrn(`page P {
ssr { api x GET /api/x { response { return data } } }
view { <main>x</main> }
}
`);
expect(diagnostics.length).toBeGreaterThan(0);
expect(diagnostics[0]!.message).toContain("apis");
});
test("client state is not flagged", () => {
const diagnostics = diagnoseWrn(`page P {
client state { count = 0 }
view { <main>x</main> }
}
`);
expect(diagnostics.filter((item) => item.severity === 1)).toEqual([]);
});
```
Use whichever diagnostic entry point the language server exports; keep the assertions identical.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/language-server/test/removed-construct-diagnostics.test.ts`
Expected: FAIL — either no diagnostic, or one that does not name `apis`.
- [ ] **Step 3: Surface the parse error as a diagnostic**
The parser already throws a message naming `apis { }` for these blocks. Ensure that message reaches the diagnostic with a position on the offending keyword rather than at offset zero.
- [ ] **Step 4: Run the tests and commit**
```bash
bun test packages/language-server
bun run format
git add packages/language-server
git commit -m "feat(language-server): flag the removed data blocks"
```
---
### Task 6: Answer the inline-diagnostics question, then the full gate
**Files:**
- Modify: whatever the observation in Step 2 shows is needed, or none
**Interfaces:**
- Consumes: Tasks 1-5.
The `apis` plan generates type assertions that make `tsc` fail when a block declares a field its endpoint rejects. **Whether that failure appears inside the `.wrn` file has never been confirmed** — it was inferred from reading source. This task settles it by looking.
- [ ] **Step 1: Rebuild the bundles**
```bash
bun run --cwd editors/vscode build
```
- [ ] **Step 2: Observe, and write down what you see**
In `examples/basic-app`, add a field to an `apis { }` entry that its endpoint does not accept, and run `bun run --cwd examples/basic-app wrnexus generate types`. Open the page in VS Code and record exactly where the error appears: on the block, only in `app/types/wrnexus.generated.api-checks.ts`, or nowhere.
Write the answer into the task report. **Do not skip this step and reason about it instead** — that is what left the question open the first time.
- [ ] **Step 3: Act on what you observed**
If the error already surfaces usefully on the block, document it and stop.
If it appears only in the generated file, map the diagnostic back: the generator knows which page and block produced each assertion, so record that mapping when emitting and use it to relocate the diagnostic.
If that mapping proves larger than this task can hold, **stop and report it as follow-up work** rather than half-building it. Say so plainly in the report.
- [ ] **Step 4: Remove the temporary field**
Revert the deliberate error and confirm `bun run typecheck` passes with zero net diff in `examples/basic-app`.
- [ ] **Step 5: Full gate**
```bash
bun run format
bun test
bun run typecheck
bun run --cwd editors/vscode build
bun run --cwd editors/vscode test
bun run check:production
```
Expected: exit 0 throughout, including `check:editor-compiler`, `check:editor-language-server`, and `check:editor-extension`.
- [ ] **Step 6: Manual pass, recorded**
Open the migrated `examples/basic-app` in VS Code and confirm: `apis { }` highlights, `api.` completes with the page's block names, hovering a name shows its method and path, and an `ssr { api … }` block is flagged. Record what you saw in the report — including anything that did not work.
- [ ] **Step 7: Commit**
```bash
git add -A
git commit -m "feat(editor): complete tooling support for the apis block"
```
---
## Notes for the executor
- **`client` is not one thing.** Removing every `client` rule from the grammar would break `client state`, `runtime = "client"`, and `client function`. Only the data-block patterns go.
- **The parser must tolerate half-typed input.** Completion fires while the document does not parse; a thrown error must become an empty completion list, not a failed request.
- **Step 2 of Task 6 is an observation, not a deduction.** Open the editor and look.
- **If a test would still pass with the code it guards deleted, it is not a test.**
@@ -0,0 +1,537 @@
# Legacy and Config Cleanup Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Delete the compatibility-flag surface, the `"legacy"` function runtime, dead migrations, and deprecated APIs before the framework's first public release.
**Architecture:** Almost all of this is deletion. Seven config keys are never read by any code, so removing them changes nothing. The one behaviour-sensitive item is the `"legacy"` function runtime, which is mapped to `"shared"` — an equivalent substitution, because an unmarked function is already emitted into both bundles.
**Tech Stack:** Bun, TypeScript, `bun:test`.
**Spec:** `docs/superpowers/specs/2026-08-19-legacy-and-config-cleanup-design.md`
## Global Constraints
- This plan removes configuration. It adds none.
- The legacy `api` block forms (bare-body `with ($data)`, and `api` inside `ssr {}` / `client {}`) are **out of scope** — they are replaced by the next plan, not deleted here.
- A removed config key must be **rejected loudly**, not silently ignored. Someone with a stale config must be told, not left believing a flag still applies.
- `bun run format` before every commit; the repo gate is `bun run check:production`.
- The editor bundles embed the compiler — rebuild with `bun run --cwd editors/vscode build` after any `packages/syntax` or `packages/compiler` change, or `check:editor-compiler` fails on a stale bundle.
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files; escaping mangles them silently. Use file editing tools.
---
### Task 1: Replace the `"legacy"` function runtime with `"shared"`
**Files:**
- Modify: `packages/syntax/src/v060.ts` (the `FunctionRuntime` type; the default at ~line 209)
- Modify: `packages/compiler/src/client-codegen.ts` (membership tests at ~lines 173 and 340)
- Modify: `packages/compiler/src/server-codegen.ts` (the `["legacy", "server", "shared"]` list)
- Modify: `packages/compiler/src/codegen.ts` (`targetFunctions`, ~line 1310)
- Test: `packages/compiler/test/legacy-runtime-removal.test.ts`
**Interfaces:**
- Produces: `FunctionRuntime` becomes `"client" | "server" | "shared"`. Later tasks and plans rely on `"legacy"` no longer existing.
**Why this is equivalent, not a behaviour change:** an unmarked `function foo()` currently parses as `"legacy"`, and both codegens include `"legacy"` in their membership tests — `["legacy", "client", "shared"]` for the browser and `["legacy", "server", "shared"]` for the server. So an unmarked function is already emitted into _both_ bundles, exactly like `shared`. `legacyDefaultRuntime` looks like it should modulate this but is never read.
- [ ] **Step 1: Write the failing test**
Create `packages/compiler/test/legacy-runtime-removal.test.ts`:
```ts
import { expect, test } from "bun:test";
import { parse } from "@wrnexus/syntax";
import { generateTargets } from "../src/targets.ts";
const SOURCE = `page Probe {
functions {
function unmarkedHelper() {
return "both";
}
client function clientOnly() {
return "browser";
}
server function serverOnly() {
return "server";
}
}
view { <main>x</main> }
}
`;
test("an unmarked function is emitted into both the browser and server modules", () => {
// This is the property the "legacy" runtime provided. Removing the variant
// must not change it.
const targets = generateTargets(parse(SOURCE));
expect(targets.browser).toContain("unmarkedHelper");
expect(targets.server).toContain("unmarkedHelper");
});
test("marked functions still go only where they belong", () => {
const targets = generateTargets(parse(SOURCE));
expect(targets.browser).toContain("clientOnly");
expect(targets.browser).not.toContain("serverOnly");
expect(targets.server).toContain("serverOnly");
expect(targets.server).not.toContain("clientOnly");
});
test("no emitted target mentions the removed legacy runtime", () => {
const targets = generateTargets(parse(SOURCE));
expect(targets.browser).not.toContain('"legacy"');
expect(targets.server).not.toContain('"legacy"');
});
```
- [ ] **Step 2: Run the test and record the baseline**
Run: `bun test packages/compiler/test/legacy-runtime-removal.test.ts`
Expected: the first two tests PASS (they describe current behaviour and must keep passing), the third may already pass. This test file is a **regression guard written before the change**, so a green run here is correct — record the output.
- [ ] **Step 3: Remove the `"legacy"` variant from the type and parser**
In `packages/syntax/src/v060.ts`:
```ts
export type FunctionRuntime = "client" | "server" | "shared";
```
And at the parse site (~line 209), change the default:
```ts
let runtime: FunctionRuntime = "shared";
```
- [ ] **Step 4: Drop `"legacy"` from every membership test**
In `packages/compiler/src/client-codegen.ts`, both occurrences:
```ts
["client", "shared"].includes(fn.runtime),
```
In `packages/compiler/src/server-codegen.ts`:
```ts
const names = ast.runtimeFunctions
.filter((fn) => ["server", "shared"].includes(fn.runtime))
.map((fn) => fn.name);
```
In `packages/compiler/src/codegen.ts`, `targetFunctions`:
```ts
const runtimes =
target === "browser" ? (["client", "shared"] as const) : (["server", "shared"] as const);
```
Search the repo for any remaining `"legacy"` in these packages and remove each — the string must not survive in `packages/syntax` or `packages/compiler`.
- [ ] **Step 5: Run the tests**
Run: `bun test packages/syntax packages/compiler`
Expected: PASS, including the three guards from Step 1. If the first two now fail, the substitution was not equivalent — stop and report rather than adjusting the test.
- [ ] **Step 6: Rebuild the editor bundles and commit**
```bash
bun run format
bun run --cwd editors/vscode build
git add packages/syntax packages/compiler editors/vscode/src
git commit -m "refactor: replace the legacy function runtime with shared"
```
---
### Task 2: Delete the compatibility surface
**Files:**
- Delete: `packages/styles/src/compatibility.ts`
- Delete: `packages/cli/src/compatibility-command.ts`
- Delete: `packages/cli/test/compatibility.test.ts`
- Modify: `packages/styles/src/config.ts` (`FunctionsConfig` ~233, `CompatibilityConfig` ~242, `AppConfig extends CompatibilityPolicy` ~249, the `functions?:` and `compatibility?:` members, the `resolveCompatibility` validation ~619, and the `CompatibilityPolicy` import ~23)
- Modify: `packages/styles/src/index.ts` (the `./compatibility.ts` exports at ~lines 39-45)
- Modify: `packages/cli/src/index.ts` (dispatch at ~line 295, help text at ~line 75)
- Modify: `packages/cli/src/create.ts` (~lines 263, 278-283)
- Modify: `packages/cli/src/update.ts` (the config insertion string at ~line 389)
- Modify: `packages/styles/test/config.test.ts` (assertions on the removed keys)
- Modify: `examples/basic-app/wrnexus.config.ts`
- Test: `packages/styles/test/removed-config-keys.test.ts`
**Interfaces:**
- Consumes: nothing from Task 1.
- Produces: `AppConfig` no longer extends `CompatibilityPolicy` and has no `compatibility` or `functions` members. `@wrnexus/styles` no longer exports `resolveCompatibility`, `isCompatibilityDate`, `CURRENT_COMPATIBILITY_DATE`, `CURRENT_FRAMEWORK_BEHAVIOUR`, `CompatibilityPolicy`, or `CompatibilityReport`.
**These seven keys are never read.** `legacyEmit`, `legacyEventProps`, `legacyComponentDiscovery`, `stringLayouts`, and `legacyDefaultRuntime` appear only in the type declaration, `create.ts`, and `update.ts`. `compatibilityDate` and `frameworkBehaviour` feed only a printed report and one validation. Removing them changes no behaviour.
- [ ] **Step 1: Write the failing test**
Create `packages/styles/test/removed-config-keys.test.ts`:
```ts
import { expect, test } from "bun:test";
import { validateConfig } from "../src/config.ts";
// A stale config must fail loudly. Silently ignoring a removed key leaves
// someone believing a flag still applies.
const REMOVED = [
{ key: "compatibilityDate", config: { compatibilityDate: "2026-08-02" } },
{ key: "frameworkBehaviour", config: { frameworkBehaviour: 1 } },
{ key: "functions", config: { functions: { legacyDefaultRuntime: "current" } } },
{ key: "compatibility", config: { compatibility: { legacyEmit: false } } },
];
for (const { key, config } of REMOVED) {
test(`a config still setting "${key}" is rejected with a message naming it`, () => {
const issues = validateConfig(config as never);
const match = issues.find((issue) => issue.path === key || issue.path.startsWith(`${key}.`));
expect(match).toBeDefined();
expect(match!.severity).toBe("error");
expect(match!.message.toLowerCase()).toContain("removed");
});
}
test("a config without those keys is accepted", () => {
const issues = validateConfig({} as never);
expect(issues.filter((issue) => issue.severity === "error")).toEqual([]);
});
```
If `validateConfig` is not the exported name in `packages/styles/src/config.ts`, use whichever function that module exports for validation and keep the assertions identical.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/styles/test/removed-config-keys.test.ts`
Expected: FAIL — the keys are currently accepted, so no issue is produced.
- [ ] **Step 3: Delete the compatibility module and its command**
```bash
git rm packages/styles/src/compatibility.ts packages/cli/src/compatibility-command.ts packages/cli/test/compatibility.test.ts
```
In `packages/styles/src/index.ts`, remove the whole `./compatibility.ts` export block (both the value exports and the `export type` line).
In `packages/cli/src/index.ts`, remove the `case "compatibility":` dispatch and the `wrnexus compatibility …` line from the help text.
- [ ] **Step 4: Remove the config members and add the rejections**
In `packages/styles/src/config.ts`: delete the `CompatibilityPolicy` import, the `FunctionsConfig` and `CompatibilityConfig` interfaces, the `functions?:` and `compatibility?:` members of `AppConfig`, `extends CompatibilityPolicy` on `AppConfig`, and the `resolveCompatibility` validation block.
Then add the rejections so a stale config fails loudly:
```ts
const REMOVED_CONFIG_KEYS = [
"compatibilityDate",
"frameworkBehaviour",
"functions",
"compatibility",
] as const;
for (const key of REMOVED_CONFIG_KEYS) {
if ((config as Record<string, unknown>)[key] !== undefined) {
issues.push({
path: key,
severity: "error",
message: "was removed; delete it from the configuration",
});
}
}
```
Place this beside the other validation pushes, using whatever local variable that function accumulates issues in.
- [ ] **Step 5: Stop scaffolding and inserting the keys**
In `packages/cli/src/create.ts`, delete the `compatibilityDate`, `frameworkBehaviour`, and `functions: { legacyDefaultRuntime: … }` lines from the generated config.
In `packages/cli/src/update.ts` (~line 389), remove `functions: { legacyDefaultRuntime: "current" },` and the whole `compatibility: { … },` fragment from the insertion string.
- [ ] **Step 6: Trim the example app config**
In `examples/basic-app/wrnexus.config.ts`, delete `compatibilityDate`, `frameworkBehaviour`, `functions`, and `compatibility`.
- [ ] **Step 7: Update the existing config tests**
`packages/styles/test/config.test.ts` asserts on the removed keys. Remove those assertions. Do not weaken any assertion that is still meaningful — if a test only existed to cover compatibility, delete the whole test.
- [ ] **Step 8: Run the tests**
Run: `bun test packages/styles packages/cli`
Expected: PASS, including the new rejection tests.
- [ ] **Step 9: Commit**
```bash
bun run format
git add -A packages/styles packages/cli examples/basic-app
git commit -m "refactor: delete the compatibility config surface"
```
---
### Task 3: Drop migrations below 0.8.0
**Files:**
- Modify: `packages/cli/src/update.ts` (all `Migration` entries with `version` below `"0.8.0"`)
- Test: `packages/cli/test/update-migration-floor.test.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: the migration list starts at `0.8.0`.
`update.ts` holds 111 migrations reaching back to `0.2.8`. The framework is pre-public and the only projects run `0.8.x`, so everything below the floor is unreachable.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/update-migration-floor.test.ts`:
```ts
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
test("no migration targets a version below 0.8.0", () => {
const source = readFileSync(join(import.meta.dir, "../src/update.ts"), "utf8");
const versions = [...source.matchAll(/version:\s*"([0-9.]+)"/g)].map((match) => match[1]!);
expect(versions.length).toBeGreaterThan(0);
const belowFloor = versions.filter((version) => {
const [major, minor] = version.split(".").map(Number);
return major! === 0 && minor! < 8;
});
expect(belowFloor).toEqual([]);
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/update-migration-floor.test.ts`
Expected: FAIL, listing the `0.2.x``0.7.x` versions.
- [ ] **Step 3: Delete the migrations below the floor**
Remove every `Migration` object whose `version` is below `"0.8.0"`, along with any helper function that becomes unused as a result. Keep every `0.8.x` entry.
After deleting, search for now-unreferenced helpers in the file and remove them too — an unused private helper is dead code, and the linter will flag it.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/cli`
Expected: PASS. Existing update tests that exercised removed migrations should be deleted with them; do not keep a test that asserts nothing.
- [ ] **Step 5: Verify `update` still runs end to end**
```bash
bun run --cwd examples/basic-app wrnexus update --dry-run
```
Expected: completes without error and reports no pending migrations for an app already at the current version. Paste the output into the commit body if it is short.
- [ ] **Step 6: Commit**
```bash
bun run format
git add packages/cli
git commit -m "chore: drop update migrations below 0.8.0"
```
---
### Task 4: Remove the deprecated compiler re-export shims
**Files:**
- Modify: `packages/compiler/src/codegen.ts` (~line 20, the `./parser.ts` import)
- Modify: `packages/compiler/src/native-codegen.ts` (~line 1, the `./parser.ts` import)
- Delete: `packages/compiler/src/parser.ts`, `packages/compiler/src/tokenizer.ts`, `packages/compiler/src/types.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: nothing new; imports move to `@wrnexus/syntax`.
**Order matters.** These three files are two-line re-exports marked deprecated, but `codegen.ts` and `native-codegen.ts` still import from them. Deleting the files first breaks the build.
- [ ] **Step 1: Repoint the imports**
In `packages/compiler/src/codegen.ts`, change:
```ts
import { VOID_ELEMENTS, type Attr, type DataMode, type PageAst, type ViewNode } from "./parser.ts";
```
to import the same names from `@wrnexus/syntax`. If the file already imports from `@wrnexus/syntax`, merge them into that one import rather than adding a second.
In `packages/compiler/src/native-codegen.ts`, change:
```ts
import type { Attr, PageAst, ViewNode } from "./parser.ts";
```
the same way.
- [ ] **Step 2: Verify nothing else imports the shims**
```bash
grep -rn "from \"./parser.ts\"\|from \"./tokenizer.ts\"\|from \"./types.ts\"" packages/compiler/src/
```
Expected: no output. If anything remains, repoint it before continuing.
- [ ] **Step 3: Delete the shims**
```bash
git rm packages/compiler/src/parser.ts packages/compiler/src/tokenizer.ts packages/compiler/src/types.ts
```
- [ ] **Step 4: Run the tests**
Run: `bun test packages/compiler && bun run typecheck`
Expected: PASS.
- [ ] **Step 5: Rebuild the editor bundles and commit**
```bash
bun run format
bun run --cwd editors/vscode build
git add -A packages/compiler editors/vscode/src
git commit -m "refactor: drop the deprecated compiler re-export shims"
```
---
### Task 5: Remove the deprecated `@wrnexus/auth` options
**Files:**
- Modify: `packages/auth/src/http/index.ts` (~lines 56-59)
- Modify: `packages/auth/src/plugin.ts` (~lines 55-58)
- Modify: `packages/auth/src/types.ts` (~line 423)
- Modify: `packages/auth/src/engine.ts` (~lines 163-165 and 179-181)
- Modify: `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, `packages/auth/test/engine.test.ts`
**Interfaces:**
- Consumes: nothing.
- Produces: nothing new. Options are removed, not renamed.
**These are our own superseded options, not an out-of-date dependency.** The current form is already what `examples/auth-showcase/app/lib/auth.ts` uses — it passes `onSignedIn` / `onSignedOut` to `createAuthEngine`, which is correct and must not change. The deprecated members are the same names on _different_ option objects.
- [ ] **Step 1: Confirm the blast radius before deleting**
```bash
grep -rn "onSuccessfullSignUp" packages/ examples/ services/ | grep -v dist/
grep -rn "onSignedIn\|onSignedOut" packages/ examples/ --include=*.ts | grep -v "packages/auth/src" | grep -v dist/
```
Expected: `onSuccessfullSignUp` has zero references. The `onSignedIn` / `onSignedOut` hits are `packages/auth/test/http.test.ts`, `packages/auth/test/plugin.test.ts`, and `examples/auth-showcase/app/lib/auth.ts`. **The example is the correct `createAuthEngine` form and must be left alone.** Record what you found; if the results differ from this, stop and report before deleting anything.
- [ ] **Step 2: Remove the option declarations**
Delete `onSignedIn` and `onSignedOut` (and their `@deprecated` comments) from the options interface in `packages/auth/src/http/index.ts` and from `packages/auth/src/plugin.ts`. Delete `onSuccessfullSignUp` from `packages/auth/src/types.ts`. Delete the `rpId` and `origin` members from both verification signatures in `packages/auth/src/engine.ts`.
Then remove the code that reads them. The `rpId` / `origin` values are already ignored — verification uses the values bound to the issued challenge — so removing them changes no behaviour.
- [ ] **Step 3: Update the tests that exercised the deprecated paths**
`packages/auth/test/http.test.ts` and `plugin.test.ts` pass the deprecated options. Rewrite each to use the `createAuthEngine` form where the test is still meaningful, and delete the test where its only purpose was to cover the deprecated alias.
`packages/auth/test/engine.test.ts` passes `rpId` / `origin` to verification. Remove those arguments; the assertions on the verification result should be unchanged, which is the evidence that the options were inert.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/auth && bun run typecheck`
Expected: PASS.
- [ ] **Step 5: Confirm no `@deprecated` markers remain in auth**
```bash
grep -rn "@deprecated" packages/auth/src/
```
Expected: no output.
- [ ] **Step 6: Commit**
```bash
bun run format
git add -A packages/auth
git commit -m "refactor: remove the deprecated auth options"
```
---
### Task 6: Full gate
**Files:**
- Modify: whatever the gate reports as stale (generated types, public API baseline, editor bundles)
**Interfaces:**
- Consumes: Tasks 1-5.
- Produces: a green `check:production`.
- [ ] **Step 1: Rebuild the editor bundles**
```bash
bun run --cwd editors/vscode build
```
The compiler and language server are embedded there and both changed.
- [ ] **Step 2: Run the full gate**
```bash
bun run format
bun test
bun run typecheck
bun run check:production
```
- [ ] **Step 3: Regenerate anything the gate reports as stale**
`check:public-api` fails when exports change — and this plan removed several from `@wrnexus/styles`. Run `bun run generate:public-api`, then **read the diff and confirm it is removals only**. An unexpected addition means something was exported by accident.
`check:generated-types` may need `bun run --cwd examples/basic-app wrnexus generate types`.
- [ ] **Step 4: Re-run the gate until green**
```bash
bun run check:production
```
Expected: exit 0.
- [ ] **Step 5: Commit**
```bash
git add -A
git commit -m "chore: regenerate baselines after the legacy cleanup"
```
---
## Notes for the executor
- **The seven config keys are dead.** If you find code that actually reads one, stop and report — the spec's central claim would be wrong and the plan needs revisiting.
- **Task 1 is the only behaviour-sensitive change.** Its first two tests describe current behaviour and must pass both before and after. If they fail after, the substitution was not equivalent; report rather than editing the test.
- **The auth example is already correct.** `examples/auth-showcase` uses `createAuthEngine({ onSignedIn })`, which is the current API, not the deprecated one.
- **If a test would still pass with the code it guards deleted, it is not a test.** Delete the implementation, watch it fail, restore it.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,496 @@
# `wrnexus update` Migration Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** One `wrnexus update` carries an existing project from today's syntax to the syntax left by the cleanup and `apis { }` plans — or refuses precisely, naming the file and the reason.
**Architecture:** These are new `Migration` entries in the existing `update.ts` framework, which already has dry-run support and a report that separates automatic changes from ones needing review. `.wrn` rewriting parses with `@wrnexus/syntax` and re-emits through `formatWrn`, both already imported there.
**Tech Stack:** Bun, TypeScript, `bun:test`, `@wrnexus/syntax`.
**Spec:** `docs/superpowers/specs/2026-08-19-update-migration-design.md`
## Global Constraints
- **A file is transformed correctly, or it is left untouched and reported.** There is no third outcome — never a partial rewrite.
- Every migration is **idempotent**: running it twice changes nothing the second time.
- **Dry-run reports exactly what a real run would change**, and writes nothing.
- A run with anything in `needsReview` or `parseFailures` **exits non-zero**, so a scripted upgrade cannot appear to succeed while leaving a project half-migrated.
- Migrations attach to the release that ships the breaking change, above the `0.8.0` floor.
- `bun run format` before every commit; the gate is `bun run check:production`.
- Do NOT use `node -e`, shell heredocs, or `sed` to write code into files.
---
### Task 1: Remove the dead config keys
**Files:**
- Modify: `packages/cli/src/update.ts` (add a `Migration`)
- Test: `packages/cli/test/migrate-config-keys.test.ts`
**Interfaces:**
- Produces: a migration with `id: "remove-dead-config-keys"`.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-config-keys.test.ts`:
```ts
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runMigrations } from "../src/update.ts";
const roots: string[] = [];
afterEach(() => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
const CONFIG = `export default {
compatibilityDate: "2026-08-02",
frameworkBehaviour: 1,
functions: { legacyDefaultRuntime: "current" },
compatibility: { legacyEmit: false, stringLayouts: false },
observability: { sampleRate: 1 },
};
`;
function project(): string {
const root = mkdtempSync(join(tmpdir(), "wrnexus-migrate-"));
roots.push(root);
mkdirSync(join(root, "app"), { recursive: true });
writeFileSync(join(root, "wrnexus.config.ts"), CONFIG);
return root;
}
test("the removed keys are deleted and the rest is kept", async () => {
const root = project();
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
expect(config).not.toContain("compatibilityDate");
expect(config).not.toContain("frameworkBehaviour");
expect(config).not.toContain("legacyDefaultRuntime");
expect(config).not.toContain("legacyEmit");
expect(config).toContain("observability");
});
test("running it twice changes nothing the second time", async () => {
const root = project();
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
const once = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: false });
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(once);
});
test("a dry run writes nothing", async () => {
const root = project();
await runMigrations({ appRoot: root, from: "0.8.0", to: "0.9.0", dryRun: true });
expect(readFileSync(join(root, "wrnexus.config.ts"), "utf8")).toBe(CONFIG);
});
```
Use whatever entry point `update.ts` exports for running migrations; if the name differs from `runMigrations`, adapt the calls and keep the assertions identical.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-config-keys.test.ts`
Expected: FAIL — the keys survive.
- [ ] **Step 3: Add the migration**
Append to the migration list in `packages/cli/src/update.ts`:
```ts
{
version: "0.9.0",
id: "remove-dead-config-keys",
description: "Delete compatibilityDate, frameworkBehaviour, functions, and compatibility",
apply(ctx) {
const file = join(ctx.appRoot, "wrnexus.config.ts");
if (!existsSync(file)) return;
const before = readFileSync(file, "utf8");
// Each key is a whole property line or block; removing the line leaves
// valid TypeScript because these are always object members.
const after = before
.replace(/^\s*compatibilityDate:.*\n/m, "")
.replace(/^\s*frameworkBehaviour:.*\n/m, "")
.replace(/^\s*functions:\s*\{[^}]*\},?\s*\n/m, "")
.replace(/^\s*compatibility:\s*\{[^}]*\},?\s*\n/m, "");
if (after === before) return;
ctx.report.changedAutomatically.push(`${file}: removed dead compatibility keys`);
if (!ctx.dryRun) writeFileSync(file, after, "utf8");
},
},
```
- [ ] **Step 4: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): migrate away the dead config keys"
```
---
### Task 2: Move `ssr { api … }` / `client { api … }` into `apis { }`
**Files:**
- Create: `packages/cli/src/migrations/apis-block.ts`
- Modify: `packages/cli/src/update.ts` (register the migration)
- Test: `packages/cli/test/migrate-apis-block.test.ts`
**Interfaces:**
- Produces: `migrateApisBlock(source: string): { source: string; changed: boolean } | { skip: string }` — a pure function over `.wrn` text, so it is testable without a filesystem. `skip` carries the human-readable reason.
Sectioned bodies carry across unchanged, because the payload is already bound to `data`.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-apis-block.test.ts`:
```ts
import { expect, test } from "bun:test";
import { migrateApisBlock } from "../src/migrations/apis-block.ts";
const SOURCE = `page Search {
client {
api searchUsers POST /api/users {
request { body { name?: string } }
response { return data.users }
error { return [] }
}
}
view { <main>x</main> }
}
`;
test("a client api entry moves into an apis block", () => {
const result = migrateApisBlock(SOURCE) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
expect(result.source).toContain("apis {");
expect(result.source).toContain("searchUsers POST /api/users");
expect(result.source).not.toContain("client {\n api");
});
test("the sections survive unchanged", () => {
const result = migrateApisBlock(SOURCE) as { source: string };
expect(result.source).toContain("return data.users");
expect(result.source).toContain("return []");
});
test("running it on migrated source changes nothing", () => {
const once = (migrateApisBlock(SOURCE) as { source: string }).source;
const twice = migrateApisBlock(once) as { source: string; changed: boolean };
expect(twice.changed).toBe(false);
expect(twice.source).toBe(once);
});
test("a name declared in both modes is skipped with a reason", () => {
const clash = `page P {
ssr { api dup GET /api/a { response { return data } } }
client { api dup GET /api/a { response { return data } } }
view { <main>x</main> }
}
`;
const result = migrateApisBlock(clash) as { skip: string };
expect(result.skip).toContain("dup");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-apis-block.test.ts`
Expected: FAIL — the module does not exist.
- [ ] **Step 3: Implement the transform**
Create `packages/cli/src/migrations/apis-block.ts`. Parse with `parse` from `@wrnexus/syntax` to find the entries and validate the file, collect every `api` entry from `ssr` / `client` blocks, detect duplicate names across modes and return `{ skip }` when found, then emit one `apis { }` block and delete the now-empty mode blocks. Re-emit through `formatWrn`.
Detect already-migrated input by checking whether the source has an `apis` block and no mode data blocks; return `{ source, changed: false }`.
- [ ] **Step 4: Register it**
Add a `Migration` with `id: "move-api-blocks"` that walks `app/**/*.wrn`, calls `migrateApisBlock`, and routes the outcome: a change goes to `changedAutomatically`, a `skip` goes to `needsReview` with the file and reason, and a `parse` failure goes to `parseFailures` with the file left untouched.
- [ ] **Step 5: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 6: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): migrate api entries into the apis block"
```
---
### Task 3: Move mode-scoped helpers into `functions { shared … }`
**Files:**
- Create: `packages/cli/src/migrations/mode-functions.ts`
- Modify: `packages/cli/src/update.ts`
- Test: `packages/cli/test/migrate-mode-functions.test.ts`
**Interfaces:**
- Produces: `migrateModeFunctions(source: string): { source: string; changed: boolean } | { skip: string }`.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-mode-functions.test.ts`:
```ts
import { expect, test } from "bun:test";
import { migrateModeFunctions } from "../src/migrations/mode-functions.ts";
const SOURCE = `page Hello {
ssr {
functions {
function userNames(users) {
return users.map((user) => user.name).join(", ")
}
}
}
view { <main>x</main> }
}
`;
test("a mode helper becomes a shared function", () => {
const result = migrateModeFunctions(SOURCE) as { source: string; changed: boolean };
expect(result.changed).toBe(true);
expect(result.source).toContain("shared function userNames");
expect(result.source).not.toContain("ssr {");
});
test("running it again changes nothing", () => {
const once = (migrateModeFunctions(SOURCE) as { source: string }).source;
const twice = migrateModeFunctions(once) as { changed: boolean; source: string };
expect(twice.changed).toBe(false);
expect(twice.source).toBe(once);
});
test("a name that already exists at page level is skipped with a reason", () => {
const clash = `page P {
functions { shared function userNames() { return "" } }
ssr { functions { function userNames(users) { return "" } } }
view { <main>x</main> }
}
`;
const result = migrateModeFunctions(clash) as { skip: string };
expect(result.skip).toContain("userNames");
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-mode-functions.test.ts`
Expected: FAIL — the module does not exist.
- [ ] **Step 3: Implement and register**
Create the module following Task 2's shape: relocate each mode-scoped function into the page-level `functions { }` with the `shared` modifier, skipping the file with a reason when a name already exists there. Register a `Migration` with `id: "move-mode-functions"` that routes outcomes to the same three report buckets.
- [ ] **Step 4: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): migrate mode-scoped helpers to shared functions"
```
---
### Task 4: Detect legacy bare-body blocks and report them — do not rewrite
**Files:**
- Create: `packages/cli/src/migrations/legacy-api-body.ts`
- Modify: `packages/cli/src/update.ts`
- Test: `packages/cli/test/migrate-legacy-api-body.test.ts`
**Interfaces:**
- Produces: `detectLegacyApiBodies(source: string): { name: string; freeIdentifiers: string[] }[]`.
**This transform is deliberately manual, and the test pins that.** A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare identifiers. Converting `return userNames(users)` needs `data.users` — but **nothing in the source distinguishes `users` (payload) from `userNames` (page helper)**. The response shape belongs to the route, which may not be typed. A migration that guessed would emit code that compiles and is wrong.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/migrate-legacy-api-body.test.ts`:
```ts
import { expect, test } from "bun:test";
import { detectLegacyApiBodies } from "../src/migrations/legacy-api-body.ts";
const SOURCE = `page Hello {
ssr {
api ssrUsers GET /api/users/ssr {
return userNames(users)
}
}
view { <main>x</main> }
}
`;
test("a legacy bare body is detected with its free identifiers", () => {
const found = detectLegacyApiBodies(SOURCE);
expect(found).toHaveLength(1);
expect(found[0]!.name).toBe("ssrUsers");
expect(found[0]!.freeIdentifiers).toContain("users");
expect(found[0]!.freeIdentifiers).toContain("userNames");
});
test("a sectioned block is not reported", () => {
const sectioned = `page P {
apis { x GET /api/x { response { return data.users } } }
view { <main>x</main> }
}
`;
expect(detectLegacyApiBodies(sectioned)).toEqual([]);
});
```
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/migrate-legacy-api-body.test.ts`
Expected: FAIL — the module does not exist.
- [ ] **Step 3: Implement detection only**
Create the module. Find `api` entries whose `sections` is absent (the bare-body form), and collect the free identifiers in the body — identifiers that are not declared locally and are not JavaScript globals. Return them. **Write no transform.**
- [ ] **Step 4: Register a report-only migration**
Add a `Migration` with `id: "report-legacy-api-bodies"` that pushes one `needsReview` entry per block, naming the file, the block, and the identifiers, and leaves the file byte-identical. Have `wrnexus update` print a short line explaining why this one is manual: the payload fields cannot be told apart from page helpers without knowing the route's response shape.
- [ ] **Step 5: Write the byte-identical test**
Add a test that runs the full migration over a fixture project containing a legacy bare body and asserts the file's contents are unchanged afterwards, and that the report names the block. **This is the most important test in the plan** — it pins that the migration does not attempt the rewrite.
- [ ] **Step 6: Run the tests**
Run: `bun test packages/cli`
Expected: PASS.
- [ ] **Step 7: Commit**
```bash
bun run format
git add packages/cli
git commit -m "feat(cli): report legacy api bodies for manual migration"
```
---
### Task 5: Exit code, output order, and the end-to-end run
**Files:**
- Modify: `packages/cli/src/update.ts` (the command's output and exit code)
- Test: `packages/cli/test/update-exit-code.test.ts`
**Interfaces:**
- Consumes: Tasks 1-4.
- [ ] **Step 1: Write the failing test**
Create `packages/cli/test/update-exit-code.test.ts` asserting that a project with a legacy bare body produces a non-zero exit, and a fully-migratable project produces zero. Use the same temp-project pattern as Task 1.
- [ ] **Step 2: Run the test to verify it fails**
Run: `bun test packages/cli/test/update-exit-code.test.ts`
Expected: FAIL — the command currently exits zero regardless.
- [ ] **Step 3: Implement the output and exit code**
Print, in order: what changed, what needs review and why, what failed to parse. Exit non-zero when `needsReview` or `parseFailures` is non-empty.
- [ ] **Step 4: Migrate the example app with the command alone**
```bash
bun run --cwd examples/basic-app wrnexus update
```
Expected: the `.wrn` pages are migrated by the tool, not by hand. **If the framework's own example cannot be migrated by the tool, the tool is not finished** — report that rather than editing the example manually.
- [ ] **Step 5: Verify the migrated example**
```bash
bun run --cwd examples/basic-app build
bun test
bun run typecheck
bun run check:production
```
- [ ] **Step 6: Commit**
```bash
bun run format
git add -A
git commit -m "feat(cli): fail the update when a project needs manual review"
```
---
## Notes for the executor
- **Never half-rewrite a file.** Parse first; on failure, record and move on. If any part of a file's transform cannot complete, skip the whole file and report it.
- **Idempotency is not optional.** Every transform detects already-migrated input.
- **Task 4 writes no transform.** If you find yourself building one, stop — the spec explains why a correct automatic answer does not exist.
---
## Post-execution note (2026-08-20)
**Step 4 of Task 5 did not prove what it was written to prove.** By the time it ran,
`examples/basic-app` had already been moved to the current syntax by hand in commit
`890d6106`, so `wrnexus update` migrated a no-op target: 0 changed, 0 needing review,
0 parse failures. The framework's own example therefore does NOT demonstrate the tool
against real legacy syntax.
The guarantee instead rests on fixture-based tests that drive the real `updateApp` /
`runUpdate` entry points over projects containing genuine `ssr {}` / `client {}` source
-- including the mixed-content page that only converges because `move-mode-functions`
is registered before `move-api-blocks`. That is adequate coverage, but it is a weaker
kind of evidence than the plan intended, and it is recorded here rather than quietly
counted as a pass.
@@ -0,0 +1,252 @@
# HTML editing support for `.wrn` files — Design
**Date:** 2026-08-18
**Status:** Approved for implementation
**Scope:** HTML autocomplete, tag closing, hover, Emmet, and folding inside `view { }` blocks.
## Goal
Writing markup in a `.wrn` file should feel like writing HTML. Today it does not: there is
syntax highlighting but no tag completion, no attribute completion, no tag closing, and no
tag-level folding.
The grammar already declares `embeddedLanguages` (`meta.embedded.block.html``html`), which is
why markup _highlights_. That mapping only affects tokenization — VS Code's HTML language
service does not run on `.wrn` documents, so none of the editing behaviour follows from it.
### Non-goals
- **HTML formatting.** See "Formatting is deliberately excluded" below.
- Editor support outside VS Code beyond what standard LSP gives for free.
- Changing `.wrn` syntax or the compiler.
## Decisions
| Question | Decision |
| ------------------- | ---------------------------------------------------------------------------- |
| Features | Tag/attribute completion, auto-close and rename tags, hover + Emmet, folding |
| Placement | Shared language server; only auto-close-on-type is VS Code-specific |
| Completion strategy | One merged list, WRNexus entries ranked above HTML |
| Region detection | Tolerant scanner over a virtual document, not the AST |
| HTML knowledge | `vscode-html-languageservice` |
| Formatting | Excluded — `formatWrn` already owns markup formatting |
## Architecture
### Virtual HTML document
New module: `packages/language-server/src/html-regions.ts`, exporting
`virtualHtmlDocument(document)`.
Everything outside a `view { }` block is replaced by whitespace of **identical length**, with
newlines preserved. The virtual document therefore has the same size and the same line/column
geometry as the source, so a position in the source _is_ the position in the virtual document.
No mapping table and no translation layer.
This is deliberately **not** the same shape as the existing `virtualTypeScriptDocument`, which
compacts code and carries line mappings back to source. Compaction is necessary there because
the output must be valid TypeScript. HTML has no such requirement, so the simpler
offset-preserving form applies, and the class of off-by-one bugs that mapping tables produce
does not arise.
**The load-bearing invariant:** `virtualHtmlDocument(doc).text.length === doc.text.length`, with
newlines at identical offsets. If this breaks, every feature reports positions off by some
amount rather than failing loudly.
### Region detection
Region detection is a tolerant scanner, **not** the `@wrnexus/syntax` parser. Completion fires
while the document is being typed, which is exactly when it does not parse. The scanner finds
`view` followed by `{` and tracks brace depth to the matching close.
Two hazards it must handle, both of which defeat a naive implementation:
- **Apostrophes in text content.** `<p>it's fine</p>` — a scanner treating `'` as a string
delimiter anywhere will consider the rest of the file one open string and lose every later
region. Quotes are tracked only inside attribute values, never in text nodes.
- **Nested braces from interpolation.** `class={cond ? "a" : "b"}` and `{{ a: 1 }}` nest, so
depth must be counted rather than scanning for the next `}`.
WRNexus-specific syntax (`@click`, `client:visible`, `{expr}`) is **not** blanked. The HTML
service tolerates unknown attributes, and blanking would cost region fidelity for no gain.
**Caching** is keyed on document URI and version, so a burst of requests from one keystroke
costs a single scan.
## Completion
### The server becomes the single authority inside view blocks
`textDocument/completion` gains a context check: a position is "in HTML" exactly when the
virtual document is non-blank there, which costs one character lookup.
**Inside a view block**, one list is assembled from two sources:
| Source | `sortText` prefix | Content |
| ------- | ----------------- | ------------------------------------------------------------------------ |
| WRNexus | `0` | Components, their props/outputs/slots, directives (`@click`, `client:*`) |
| HTML | `1` | Tags, attributes, attribute values |
`sortText` drives ordering independently of the label, so components rank above HTML tags
without filtering anything out. **Outside a view block**, behaviour is unchanged: WRN keywords
plus workspace items.
The server already indexes components, props, outputs, and slots
(`buildWorkspaceCompletionItems` in `packages/language-server/src/workspace.ts`), so both halves
of the merge are already available to it.
**Deduplication on exact label match, WRNexus wins.** A component named `Table` and the HTML
`table` differ in case and both survive; a component that genuinely shadows an HTML tag name
resolves to the component.
### Trigger characters
The server currently declares `["<", "@", ":", "."]`. Attributes and values additionally need
`" "`, `"="`, `"\""`, and `"/"`.
### This fixes an existing bug
The extension's `completion.js` registers its own provider with `<` among its trigger
characters, and the language server answers `textDocument/completion` as well. VS Code
concatenates both today, producing duplicate entries and unpredictable ordering before HTML is
involved at all.
As part of this work the extension's provider returns nothing when the position is inside a view
block, and keeps its current behaviour elsewhere. One owner per context.
**Consequence to accept knowingly:** the server becomes authoritative for the richest completion
context, so future component-intelligence work belongs in the server rather than in
`completion.js`.
## Hover
`textDocument/hover` answers from the HTML service over the virtual document when the position
is inside a view region, giving MDN documentation for tags and attributes. Outside a view
region, existing hover behaviour is unchanged.
Where a position resolves to a WRNexus component or prop, the component's own detail wins over
any HTML entry of the same name, matching the completion precedence rule above.
## Tag handling
### Linked editing is standard LSP
Renaming `<div>` and having `</div>` follow is `textDocument/linkedEditingRange` (LSP 3.16), so
it lives in the shared server like everything else.
### Auto-close on type is the one client-side piece
LSP has no request for "close this tag as I type". VS Code's own HTML extension implements it
client-side, and this follows the same shape:
1. The extension subscribes to `onDidChangeTextDocument`, filtered to `wrn` documents.
2. When the typed character is `>` or `/`, it sends a custom request, `wrn/tagComplete`.
3. The server runs the HTML service's `doTagComplete` against the virtual document and returns a
snippet or `null`.
4. The client inserts it with `insertSnippet`, so the cursor lands between the tags.
The decision stays server-side because it needs parse knowledge: void elements (`<br>`, `<img>`,
`<input>`) must not be closed, and an already-closed tag must not be closed twice. Returning
`null` outside a view region is what stops it firing inside `functions { }` or `style { }`.
Component tags come along for free: `<Card>` closes to `</Card>` because the HTML service closes
unknown tags like any other, and `<Card /` completes to `<Card />` through the same `/` path.
**New setting:** `wrnexus.html.autoClosingTags`, default `true`, following the existing
`wrnexus.*` naming.
### Emmet
A manifest change: `emmet.includeLanguages: { "wrn": "html" }` in `contributes.configurationDefaults`.
**Known limitation:** `emmet.includeLanguages` is per-language, not per-region, so Emmet is also
live inside `functions { }` and `style { }` blocks. VS Code offers no way to scope it to a
region. Emmet only expands on Tab against an abbreviation pattern, so misfires are rare, but the
edge is real.
## Folding
`textDocument/foldingRange` in the server returns tag-level ranges from the HTML service over
the virtual document, filtered to view regions.
Today folding comes only from `language-configuration.json` markers, which work at block level
(`page`, `component`, `view`, braces). Markup does not fold, so a long `<table>` cannot be
collapsed. VS Code merges marker-based folding with provider ranges, so block folding continues
to work unchanged and tag folding appears inside markup.
**One rule:** return ranges only where the virtual document is non-blank. A range spanning
outside a view region would let a fold swallow a brace boundary.
## Formatting is deliberately excluded
`formatWrn` (`packages/syntax/src/formatter.ts`) is 927 lines, iterates to a fixed point with
cycle detection, and already handles tags, attribute wrapping, `multilineAttributes`, and
`printWidth`. It is a markup formatter that understands WRNexus syntax.
Adding HTML formatting would do two harmful things:
- **Two formatters would fight.** Output would depend on which ran last.
- **It would mangle syntax it does not model.** `@click={handler}` and `client:visible` are not
HTML attributes, and an HTML formatter is free to rewrite spacing inside them.
If markup formatting is unsatisfying, the fix is improving `formatWrn`. That is separate work.
## Dependencies
`vscode-html-languageservice` becomes a dependency of **both** `packages/language-server` and
`editors/vscode`.
The editor bundler (`scripts/build-editor-language-server.mjs`) bundles only workspace sources
and passes other `require`s through to Node, so the package must be resolvable at runtime from
the extension. `editors/vscode` currently ships exactly one runtime dependency
(`vscode-languageclient`); this adds the second.
`check:editor-language-server` already verifies the bundled `.cjs` starts under Node, so a
missing or unresolvable dependency fails the gate rather than shipping a broken VSIX.
## Testing
### Region scanner (`packages/language-server/test/`)
- **The invariant**, property-style across fixtures: virtual text length equals source length and
newlines sit at identical offsets.
- **Apostrophes in text**: `<p>it's fine</p>` followed by a second view block — both regions
found.
- **Nested interpolation**: `class={cond ? "a" : "b"}` and `{{ a: 1 }}` do not end the region.
- **Broken markup**: `<div class="` mid-typing still yields a region. This is the normal case for
completion, not an edge case.
- **Multiple view blocks**, and files with none.
### Completion
- Inside a view block: both sources present, WRNexus `sortText` ordering first.
- Outside a view block: response identical to current behaviour — the guard proving non-markup
contexts are undisturbed.
- Collision: a component named `Table` yields one entry, the component.
### Tag handling
- `<div>``</div>`; `<br>` → nothing; `<Card /``/>`; outside a view region → `null`.
- Linked editing returns ranges covering both the opening and closing tag names.
### Hover
- Inside a view region, a known tag returns HTML documentation.
- A component name returns the component detail, not an HTML entry of the same name.
### Folding
- Every returned range lies inside a view region.
- Block-level marker folding still works.
### Toolchain guards
- `check:editor-language-server` passes with the new dependency (bundle starts under Node).
- Manifest assertion that `emmet.includeLanguages` maps `wrn``html`, alongside the existing
marketplace checks in `editors/vscode/test`.
## Deferred
- HTML formatting — see above; improve `formatWrn` instead.
- Moving the remaining `completion.js` component intelligence into the server. This design only
requires it to stand down inside view blocks; relocating the rest is follow-up work.
@@ -0,0 +1,250 @@
# The `apis { }` block and location-transparent dispatch — Design
**Date:** 2026-08-19
**Status:** Approved for implementation
**Scope:** One container block for API declarations, callable from anywhere as `api.<name>(input)`,
dispatched in-process on the server and over `fetch` in the browser.
**Depends on:** `2026-08-19-legacy-and-config-cleanup-design.md`. That spec removes the
compatibility surface and the `"legacy"` function runtime; this one removes the legacy `api` forms
by replacing them.
## Goal
Today a `.wrn` page has several unrelated ways to reach data: an `api` block inside `ssr {}`, an
`api` block inside `client {}`, a `server function` over RPC, a `load server` block, and hand-written
`fetch`. Each has different placement, different capabilities, and a different call shape. The
result is that "how do I fetch this?" has no single answer, and the answer that is right depends on
where the code happens to sit.
This collapses the API half of that into one declaration and one call, and draws a line a developer
can hold in their head:
- **`api.<name>()`** — call an API route that exists as a real HTTP endpoint.
- **`server.<name>()`** — call server-side logic that has no public surface.
The question becomes "is there a route?", not "where am I running?". `server.<name>()` is unchanged
by this spec.
### Non-goals
- Changing `server function`, actions, or `load` blocks. They keep working exactly as they do.
- External or third-party API targets. Still this app's `/api/*` routes only, preserving
`isSafeApiPath`.
- Any new configuration key. This spec adds none.
- Author-settable request headers, still excluded.
## Decisions
| Question | Decision |
| ---------------------- | ---------------------------------------------------------------------- |
| Container | Page-level `apis { }`, matching `functions { }` |
| Declaration | Mode-less — no `ssr` / `client` prefix |
| Call | `api.<name>(input)` from any context |
| Dispatch | Chosen at build time: in-process on the server, `fetch` in the browser |
| Server request context | `AsyncLocalStorage` |
| Browser emission | Only blocks the client actually calls |
| Render binding | `api="name"`, `api="name()"`, `api="name({ … })"` |
| Old forms | Removed and replaced |
## Syntax
```wrn
apis {
searchUsers POST /api/users {
request {
body {
name?: string
age?: number
}
}
response { return data.data.users }
error { return [] }
}
listTeams GET /api/teams {
response { return data.data.teams }
}
}
```
`GET` and `HEAD` declare `parameters`, which become a query string; other methods declare `body`,
sent as JSON. The path stays a plain literal so `isSafeApiPath` is satisfied without relaxing it.
Names are unique per page — `codegen.ts` already rejects duplicates, and that stays.
### Why the container
`functions { }` puts the modifier first inside a container named for the concept:
`functions { client function x() }`. Today's api form inverts that — `client { api x … }` — and is
the only construct in the language shaped that way, which is why APIs are hard to find in a page.
`apis` (plural) is the container; `api` remains the call namespace and the binding attribute.
## Calling
```wrn
functions {
client async function search(): Promise<void> {
users = await api.searchUsers({ name: nameFilter })
}
}
load server directory {
return await api.searchUsers({ name: ctx.url.searchParams.get("name") ?? "" })
}
```
The same call works in client functions, `load` blocks, actions, and server functions. Nothing at
the call site says where it runs.
### Dispatch
Dispatch is decided at build time, not by a runtime check. The compiler emits an `api` object into
each execution context, with the same member names and different transports behind them:
- **Browser** — the existing `callApi` transport: query string or JSON body, `credentials:
"same-origin"`, `x-csrf-token` on non-GET, the established failure contract.
- **Server**`callApiFromContext`, which dispatches to the route in-process. No network hop, no
serialisation round trip, and the caller's cookies, session, and locals are already forwarded.
Two objects that never meet, so nothing ships to the browser that only the server uses, and the
runtime never branches on `typeof window` for something known at compile time.
### `callApiFromContext` must learn to carry input
It currently builds `new Request(apiUrl, { method, headers })` — no body, no query string. It has to
assemble the request the same way the browser transport does, from the same rules, or the two sides
will disagree about what an identical call sends. **The assembly rules must be shared, not
reimplemented**: a second copy will drift, and the drift will be silent because each side is tested
separately.
### The request context
A server-side call needs `ctx` to resolve the URL and forward cookies and session. `ctx` is not
uniformly available: `load` blocks have it, schema actions have it as a second parameter, plain
actions and server functions have neither.
The server stores the request context in an `AsyncLocalStorage` at request entry, and the server
`api` object reads it. This is new machinery — the framework uses none today — and it must be
established in both the dev server and the production server, or a call that works in development
fails in production.
When no context is present, the call throws with a message naming the block and explaining that an
API call needs a request context — never a silent `undefined`.
## Emission
A block's `response` and `error` bodies are page code. They ship to the browser **only when a
client-side call to that block exists**. The compiler already knows which `api.<name>()` calls
appear in client functions.
This keeps server-only transforms off the wire and the client bundle proportional to what it uses.
The consequence to know: adding the first client call to a block starts shipping that block's
bodies. Anything secret belongs in the endpoint, not in a `response` body.
## Render binding
A block can be bound into markup, which calls it during render and substitutes the result:
```wrn
<p api="listTeams">loading…</p>
<ul api="searchUsers({ name: nameFilter })">
{#each searchUsers as person}<li>{person.name}</li>{/each}
</ul>
```
Three accepted forms: `api="name"`, `api="name()"` — equivalent — and `api="name({ … })"`, which
passes arguments. The argument expression is evaluated in the same scope as other view expressions
at render time. This mirrors `@click="search()"`, so it introduces no new escaping or naming rules.
### The edge this creates, stated plainly
A block that is both render-bound and called from code **runs twice** — once for the binding, once
for the call. They are two different lifecycles wearing one name, and no deduplication is attempted:
a render-time fetch and a user-triggered fetch are usually meant to be different requests, and
silently collapsing them would be worse than the duplication. Authors binding a block _and_ calling
it should expect two requests.
## Replacing the old forms
`ssr { … }` and `client { … }` data blocks are removed. Each could contain only two things, and both
have a home:
| Old | New |
| ------------------------------------------------ | ------------------------------------------------------ |
| `ssr { api x … }` / `client { api x … }` | `apis { x … }` |
| `ssr { functions { function helper() } }` | `functions { shared function helper() }` |
| legacy bare-body `api x GET /p { return users }` | `apis { x GET /p { response { return data.users } } }` |
The legacy bare body injected the payload with `with ($data ?? {})`, which is **untypeable**
TypeScript cannot see through `with`, and that is the entire reason sectioned blocks bind a named
`data`. Removing the legacy form removes that fork: one payload binding, typed.
`client state { }` is a different construct that shares the keyword and is **not** affected.
`examples/basic-app/app/pages/hello.wrn` uses both old forms and is the migration's worked example.
## Type safety
Assertions are generated into `app/types/wrnexus.generated.api-checks.ts` and checked by the
project's own `tsc`, as established. Two changes follow from mode-less declarations:
- The current generator skips blocks whose `mode !== "client"`. That skip exists because an `ssr`
block could never declare a `request`. Mode-less blocks invalidate the reasoning, so **every block
with declared fields gets an assertion**.
- The zero-field skip stays: a block with no declared fields has nothing to check, and asserting
`Record<string, never>` against a contract fails spuriously.
## Known edges
- **`state api` stops working.** The name is currently excluded from destructuring only when a page
has client api blocks, so pages without them keep using it. Once `api` is universal that
protection goes, and a page with `state api` breaks. It is a build-time failure, not silent.
- **Every block is browser-reachable in principle.** The routes were already publicly reachable by
`fetch`, so this exposes no new surface — but a block is no longer implicitly server-only by
virtue of its placement.
## Testing
**Parser**
- `apis { }` parses multiple mode-less entries; duplicate names are rejected.
- `ssr { api … }` and `client { api … }` are rejected with a message naming the replacement.
- All three binding forms parse, including an argument expression containing a nested object.
**Dispatch**
- A client-side call issues one `fetch` with the expected URL, method, body, and CSRF header.
- A server-side call dispatches in-process and issues **no** network request — asserted by observing
that no fetch occurs, not merely that the result is right.
- A server-side call with no request context throws a message naming the block.
- The same declared input produces the same request on both sides — the shared-assembly guard.
**Emission**
- A block called only from server code does not appear in the browser module.
- A block called from a client function does.
**Render binding**
- Each of the three forms renders the resolved value.
- A bound block that is also called issues two requests, pinning the documented edge.
**Type safety**
- A block declaring a field its endpoint rejects fails `bun run typecheck`, asserted by running
`tsc` and reading its diagnostics — not by matching generated text.
**End to end**
- `examples/basic-app` migrated to `apis { }`, driven in a browser: a client call updates state, a
render-bound block appears in the served HTML, and a deliberately failing call takes the `error`
path.
## Deferred
- External API targets, and the allowlist and credential handling they need.
- Author-settable headers.
- Deduplicating a render-bound block against a code call.
- Response caching and request de-duplication.
@@ -0,0 +1,131 @@
# Editor support for the new syntax — Design
**Date:** 2026-08-19
**Status:** Approved for implementation
**Scope:** Language server and VS Code extension updated for `apis { }`, the `api.<name>()` call, and
the removal of the mode data blocks.
**Depends on:** `2026-08-19-apis-block-design.md`. The syntax must exist before the editor can
describe it.
## Goal
A syntax change that the editor does not know about is worse than no change: valid code is
red-underlined, removed constructs still autocomplete, and the new block gets no highlighting. This
spec keeps the tooling level with the language.
It also settles a question left open by earlier work: whether type errors from the generated
assertions actually appear inside the `.wrn` file, or only in the generated file. That was never
verified — it was inferred from reading source — and inference is not good enough for the thing
developers rely on to tell them their code is wrong.
### Non-goals
- New editor features unrelated to this syntax change.
- Editors other than VS Code beyond what standard LSP provides.
- HTML formatting — `formatWrn` still owns markup.
## What exists
- **Grammar:** `editors/vscode/syntaxes/wrn.tmLanguage.json` names block keywords directly —
`api` appears 4 times, `client` 6, `server` 5, `ssr` once.
- **Keyword list:** `WRN_KEYWORDS` in `packages/language-server/src/index.ts` drives completion and
includes `api` but not `apis`.
- **Extension completions:** `editors/vscode/src/completion.js` carries block snippets.
- **Server features:** completion, hover, folding, linked editing, tag completion, and TypeScript
diagnostics over a virtual document.
## Changes by surface
### Grammar
Add `apis` as a block keyword. Remove the `ssr` / `client` **data block** patterns, keeping `client`
and `server` where they mean other things — `client state { }`, the `runtime` values, and the
function modifiers in `functions { }`. This is the change most likely to over-reach: `client` is one
word with several jobs in this language, and blanket removal would un-highlight constructs that
still exist.
Highlight an `apis` entry's shape — name, method, path — so a declaration reads as a declaration
rather than as loose identifiers.
### Keyword and block completion
- `apis` joins `WRN_KEYWORDS`.
- A snippet for the container and a snippet for an entry, including the `request` / `response` /
`error` sections, so the shape is discoverable without the docs.
- `ssr` and `client` data-block snippets are removed from `completion.js`. Offering a construct the
compiler rejects is worse than offering nothing.
### Call completion — the feature worth building
Inside a function body, `api.` should complete to the names declared in that page's `apis { }`
block, with the method and path as detail. The server already indexes the document to build
completions, and the block names are in the AST.
This is where the syntax pays off in the editor: the set of legal calls is knowable, so the editor
should know it. Without it, `api.` is an empty namespace and every call is typed from memory.
Hovering a name inside `api.<name>()` shows its method, path, and declared request fields.
### The `api=` attribute in markup
`api="searchUsers({ name: nameFilter })"` is an attribute whose value is a call expression. The HTML
service must not flag it as an unknown attribute, and the expression must not be treated as plain
text. Completion inside the quotes offers the page's block names, matching the `api.` behaviour.
### Diagnostics for removed constructs
An `ssr { api … }` or `client { api … }` block gets a diagnostic naming `apis { }` as the
replacement, positioned on the block keyword. The compiler already rejects these; the editor should
say so while typing rather than at build time, and it should say what to do instead.
## Inline type errors — verifying, not assuming
The `apis` spec generates assertions into `app/types/wrnexus.generated.api-checks.ts`, and `tsc`
fails when a block declares a field its endpoint rejects. Whether that failure surfaces **inside the
`.wrn` file** has never been confirmed.
This spec resolves it in two steps, in order:
1. **Observe the current behaviour.** With a deliberately wrong field in place, open the page in VS
Code and record where the error appears: on the block, only in the generated file, or nowhere.
2. **Act on what is observed.** If the error already surfaces usefully, document it and stop. If it
appears only in the generated file, map the diagnostic back to the block that produced it — the
generator knows which page and block each assertion came from, so the mapping is available if it
is recorded rather than discarded.
If step 2 proves larger than this spec can hold, it becomes its own work, and the spec says so
plainly rather than leaving an unfinished feature implied. **Nothing here should claim inline
diagnostics work until someone has seen them work.**
## Testing
**Grammar** — a fixture page using `apis { }`, `client state { }`, `functions { shared function }`,
and `runtime = "client"` tokenizes correctly; `client` keeps its highlighting everywhere it is still
valid. This is the guard against over-reaching removal.
**Completion**
- `apis` is offered at page level; `ssr` / `client` data blocks are not.
- `api.` inside a function body offers the page's declared names with method and path.
- Inside `api="…"` in markup, the same names are offered.
- Outside those contexts, completion is unchanged — the guard that non-API editing is undisturbed.
**Hover** — a name inside `api.<name>()` reports its method, path, and request fields.
**Diagnostics** — an `ssr { api … }` block produces a diagnostic naming `apis { }`, positioned on
the block keyword.
**Bundles** — `check:editor-compiler`, `check:editor-language-server`, and
`check:editor-extension` pass. These embed the compiler and language server, so they must be rebuilt
after the syntax change; a stale bundle fails the gate.
**Manual, and recorded in the implementation notes** — open the migrated `examples/basic-app` in VS
Code: `apis { }` highlights, `api.` completes, a removed construct is flagged, and the inline
type-error question above is answered by observation.
## Deferred
- Mapping generated assertion diagnostics back into `.wrn`, if step 2 above proves too large.
- Moving the remaining component intelligence out of `completion.js` and into the server.
- Editors other than VS Code.
@@ -0,0 +1,149 @@
# Legacy, deprecated, and unused-config cleanup — Design
**Date:** 2026-08-19
**Status:** Approved for implementation
**Scope:** Remove the compatibility-flag surface, the `"legacy"` function runtime, dead migrations,
and deprecated APIs — before the framework's first public release.
## Goal
WRNexus carries compatibility machinery for a public it does not yet have. Every branch of it is
either switched off in the only apps that exist, or wired to nothing at all. Removing it now costs
almost nothing; removing it after release costs a major version and other people's time.
## Why this is safe now
Two pieces of evidence, both verified rather than assumed:
**The only consumers already run without it**, which matters for the config files themselves even
though the keys are inert. `D:\Company\wrnexus\apps\admin` and
`D:\Company\wrnexus\apps\web` — both test projects, neither deployed — set every compatibility flag
to `false` and `legacyDefaultRuntime: "current"`. They are already on the modern path; removing the
flags means deleting the lines that say "off".
**None of the seven keys change any behaviour.** Verified by tracing every reference, not by
reading the types. `legacyEmit`, `legacyEventProps`, `legacyComponentDiscovery`, `stringLayouts`,
and `legacyDefaultRuntime` appear in exactly three places each: the type declaration in
`config.ts`, the scaffolder in `create.ts`, and the insertion in `update.ts`. **No compiler,
codegen, or runtime code reads any of them.** They are written into every generated config and then
ignored.
The remaining two are the same story with more machinery. `resolveCompatibility` (`packages/styles/src/compatibility.ts`)
produces a report — an effective date, a behaviour number, and advisory strings. Tracing every
consumer: the `wrnexus compatibility` command prints it, and `config.ts` raises one validation error
when the configured date is _newer_ than the CLI supports. **No compiler branch and no runtime
behaviour reads `effectiveDate` or `effectiveBehaviour`.** The mechanism is scaffolding that was
never connected.
### Non-goals
- **The legacy `api` block forms.** Bare-body blocks using `with ($data)`, and `api` entries inside
`ssr {}` / `client {}`, are _replaced_ rather than deleted — that is the next spec's job. Removing
them here would leave a gap with no working mechanism.
- Adding any configuration key. This spec only removes them.
- Changing any behaviour that is currently switched on.
## What gets removed
| Item | Where | Why it goes |
| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
| `compatibility: { legacyEmit, legacyEventProps, legacyComponentDiscovery, stringLayouts }` | `packages/styles/src/config.ts` (`CompatibilityConfig`) | Never read by any code |
| `functions: { legacyDefaultRuntime }` | `packages/styles/src/config.ts` (`FunctionsConfig`) | Never read by any code |
| `compatibilityDate` | `packages/styles/src/compatibility.ts` (`CompatibilityPolicy`) | Gates nothing |
| `frameworkBehaviour` | same | Gates nothing |
| `resolveCompatibility`, `CompatibilityReport`, `isCompatibilityDate`, `CURRENT_COMPATIBILITY_DATE`, `CURRENT_FRAMEWORK_BEHAVIOUR` | `packages/styles/src/compatibility.ts` | Whole module serves only the two dead keys |
| `wrnexus compatibility <check\|explain\|upgrade>` | `packages/cli/src/compatibility-command.ts`, dispatch at `packages/cli/src/index.ts:295`, help text at `:75` | Reports on removed keys |
| `"legacy"` variant of `FunctionRuntime` | `packages/syntax/src/v060.ts`, branches in `packages/compiler/src/client-codegen.ts` and `server-codegen.ts` | An unmarked function becomes `shared` (see below) |
| Migrations below `0.8.0` | `packages/cli/src/update.ts` | 111 migrations reach back to `0.2.8`; no project exists below `0.8.x` |
| Deprecated re-export shims | `packages/compiler/src/{parser,tokenizer,types}.ts` | Two lines each, re-exporting `@wrnexus/syntax` |
| Deprecated `@wrnexus/auth` options | `engine.ts` (4 sites), `http/index.ts` (2), `plugin.ts` (2), `types.ts` (1) | See "Deprecated auth options" |
## The `"legacy"` function runtime
`FunctionRuntime` is `"legacy" | "client" | "server" | "shared"`. `"legacy"` is what an _unmarked_
`function foo()` gets, and `legacyDefaultRuntime` decides how it behaves. Both codegens then test
membership: `["legacy", "client", "shared"]` for the browser and `["legacy", "server", "shared"]`
for the server — which is to say **an unmarked function is currently emitted into both bundles,
exactly like `shared`.**
`legacyDefaultRuntime` looks like it should modulate this, but it is never read (above), so the
mapping is unconditional: unmarked is always `"legacy"`, and `"legacy"` is always emitted to both
bundles.
So the removal is mechanical: delete the `"legacy"` variant, and parse an unmarked function as
`"shared"`. The emitted output for every existing unmarked function is unchanged, in every
configuration. `FunctionRuntime`
becomes `"client" | "server" | "shared"`, and the membership tests lose one element each.
This is the one item where behaviour could drift if done carelessly, so its test is explicit: an
unmarked function must still appear in both the browser and server modules.
## Deprecated re-export shims
`packages/compiler/src/parser.ts`, `tokenizer.ts`, and `types.ts` are two-line files re-exporting
`@wrnexus/syntax`. They are marked deprecated, but **`codegen.ts` and `native-codegen.ts` still
import from them**, so deleting the files is not enough — those imports must be repointed at
`@wrnexus/syntax` first. Removing the files without that step breaks the build.
## Deprecated auth options
`@wrnexus/auth` carries nine `@deprecated` markers. These are **our own superseded options**, not an
out-of-date dependency — there is no newer version to move to, only newer options we already added.
Removing them means deleting the old aliases and moving the few call sites that still use them.
| Group | Where | Replacement | Still used? |
| --------------------------------------------------------------------- | ---------------------------- | ------------------------------------------- | ----------------------------------------------------------- |
| `onSignedIn` / `onSignedOut` on HTTP route options and plugin options | `http/index.ts`, `plugin.ts` | the same names on `createAuthEngine({ … })` | only `packages/auth/test/http.test.ts` and `plugin.test.ts` |
| `onSuccessfullSignUp` (misspelled alias) | `types.ts` | `onSuccessfulSignUp` | nowhere — zero references |
| `rpId` / `origin` on passkey verification | `engine.ts` (4 sites) | values bound to the issued challenge | `packages/auth/test/engine.test.ts` |
Two things this table settles:
- **The recommended form is already in use.** `examples/auth-showcase/app/lib/auth.ts` passes
`onSignedIn` / `onSignedOut` to `createAuthEngine`, which is the _current_ API. The deprecated
members are the same names on different option objects, so the example needs no change.
- **Neither test app uses any of them.** Nothing in `D:\Company\wrnexuspps` references these
options.
So the blast radius is three test files inside `packages/auth`, which are exercising the deprecated
paths and are updated or removed alongside them. The `rpId` / `origin` options are already ignored at
runtime — verification uses the values bound to the issued challenge — so removing them changes no
behaviour, only the shape of the call.
## Migration
`examples/basic-app` and both test apps need one pass each:
1. Delete `compatibility`, `functions`, `compatibilityDate`, and `frameworkBehaviour` from
`wrnexus.config.ts` — seven lines per app.
2. `packages/cli/src/create.ts` stops scaffolding those keys, so new apps get a shorter config.
No `.wrn` source changes. Nothing in this spec alters page syntax.
The removed `0.2.x``0.7.x` migrations mean a project below `0.8.0` can no longer be upgraded by
`wrnexus update`. No such project exists, and rescuing one would be a manual job either way.
## Testing
- **The `"legacy"` runtime removal is behaviour-preserving**: an unmarked function still appears in
both the browser and server modules. This is the assertion most worth writing, because it is the
only removal that could silently change output.
- **Config rejects the removed keys** rather than ignoring them, so a stale config fails loudly with
a message naming the key. A silently-ignored key would leave someone believing a flag still
applies.
- **`create.ts` scaffolds a config without them**, asserted against the generated file.
- **`wrnexus update` still runs** with the pre-`0.8.0` migrations gone, and reports correctly for an
app already at the current version.
- **`examples/basic-app` builds and its suite passes** after its config is trimmed — the end-to-end
guard that nothing depended on the removed surface.
- **The full gate** (`bun run check:production`) passes, including the editor bundles, which embed
the compiler and must be rebuilt after `FunctionRuntime` changes.
## What we give up
Deleting `compatibilityDate` and `frameworkBehaviour` removes the standard escape hatch for changing
a default after going public — the mechanism that lets an existing app keep old behaviour by pinning
a date. Today it is wired to nothing, so it protects nobody, and an unused mechanism rots rather
than matures. If a gate is needed later it can be reintroduced deliberately, against a real
behaviour change, instead of being carried empty. This is a considered trade rather than a free
deletion.
@@ -0,0 +1,276 @@
# Typed, callable `api` blocks for `.wrn` files — Design
**Date:** 2026-08-19
**Status:** Approved for implementation
**Scope:** A sectioned `api` block that declares a typed request, transforms the response, and
handles failure — callable on demand from client code.
## Goal
Calling this application's own API routes from a `.wrn` page should be declarative and
type-checked. Today it is neither: the `api` block takes no parameters at all, so anything
carrying a value from the page is written as a hand-rolled `fetch` — query-string assembly,
JSON headers, CSRF, status checks, and a `try/catch` repeated at every call site.
### What the current block cannot do
These are implementation facts, not gaps in documentation:
- **No query string.** `isSafeApiPath` (`packages/dev-server/src/runtime.ts`) rejects any path
containing `?` or `#`.
- **No interpolation.** `readPath()` reads until whitespace or `{`, so `/api/users?name={filter}`
ends the path at the brace and the remainder is parsed as the block body.
- **No request body.** The caller builds `new Request(apiUrl, { method, headers })` — there is no
parameter a payload could occupy, whatever method is named.
- **Fetch-once.** `setupCsrFetch` sets an `__wrnexusCsrFetch` flag and returns early on any later
pass, so a binding cannot be re-run.
### Non-goals
- External or third-party APIs. Targets are restricted to this app's `/api/*` routes, preserving
the existing `isSafeApiPath` guarantee.
- Replacing `server function`. That remains the way to run arbitrary server logic over RPC.
- Author-settable headers. See "Why `headers` is excluded".
- Parameterised server-render fetching. See "The SSR boundary".
## Decisions
| Question | Decision |
| ---------------- | ------------------------------------------------------------------------ |
| Trigger | `client {}` blocks are callable on demand; `ssr {}` stays render-time |
| Targets | This app's `/api/*` routes only |
| Execution | Decided by the enclosing mode, not a modifier |
| Request values | Declared fields, supplied at the call site |
| Type source | Route contract when available, declared types otherwise (with a warning) |
| Type enforcement | `tsc`, via assertions generated into `wrnexus.generated.api-checks.ts` |
| Failure | `error {}` converts a failure to a value; without it, the call rejects |
## Syntax
```wrn
client {
api searchUsers POST /api/users {
request {
body {
name?: string
age?: number
designation?: string
}
}
response {
return data.users
}
error {
return []
}
}
}
```
Called as `const users = await api.searchUsers({ name: nameFilter.trim() })`. The `api` namespace
joins those already in client scope (`server`, `output`, `props`, `refs`), so it reads the same way
as `server.searchUsers()`.
`GET` blocks declare `parameters` rather than `body`; the compiler appends them as a query string at
call time. The path in source stays a plain literal, so `isSafeApiPath` is satisfied without
relaxing it.
### Backward compatibility
A bare body keeps meaning "this is the response block", unchanged:
```wrn
ssr {
api ssrUsers GET /api/users/ssr {
return users.map((user) => user.name).join(", ")
}
}
```
The rule is **bare body = legacy untyped block; sections = typed block.**
The two forms reach the payload differently, and the reason is load-bearing rather than cosmetic.
The legacy form injects the response with `with ($data ?? {})`, which is why bare `users` resolves.
**`with` is untypeable** — TypeScript cannot see through it — so a typed `response` block is
impossible in that form. Sectioned blocks therefore bind the payload to `data`, specifically so
`tsc` can check `data.users` against the route's contract.
### Why `headers` is excluded
Own-route calls are same-origin, so cookies are already attached; `content-type` and `accept` follow
from whether the block has a body; and CSRF is attached by the runtime (below). What remains for an
author to set is mostly credentials, which do not belong in page source. Excluded from v1 pending a
concrete case.
## Type safety
### The constraint that shapes this
`examples/basic-app/tsconfig.json` uses `include: ["app"]` and excludes `.wrnexus-*`, and
`wrnexus build` never invokes `tsc`. **Generated build artifacts are not type-checked.** Compiling
the block into a typed client and expecting `tsc` to catch mismatches would therefore check nothing.
What _is_ type-checked is application source under `app/`. Enforcement goes there.
**Corrected 2026-08-19, during implementation.** This section originally placed the assertions in
`app/types/wrnexus.generated.d.ts`. That is inert: the root `tsconfig.json` sets
`skipLibCheck: true`, which exempts the _contents_ of every `.d.ts`, so an assertion written there
can never raise a `tsc` error. Proven by forcing `skipLibCheck: false`, under which the same
assertion fires as `TS2344`. The reasoning was right and the file was wrong. Per-block assertions
are emitted into a real `.ts` file instead — `app/types/wrnexus.generated.api-checks.ts` — which
`skipLibCheck` does not exempt and which `include: ["app"]` compiles. The helper types stay in the
`.d.ts`, where being declarations is correct.
### Three pieces
**1. Helper types**, extending what the generator already emits (`ApiRoute`, `ApiContracts`,
`ApiContract`):
```ts
type AssertAssignable<Actual, Expected> = Actual extends Expected ? true : never;
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
```
**2. Per-block assertions**, generated into `app/types/wrnexus.generated.api-checks.ts`. `wrnexus generate types` already parses
`.wrn` sources to build the route list, so it can read each block's declared fields and emit:
```ts
type __wrn_check_searchUsers = AssertAssignable<
{ name?: string; age?: number },
ApiInput<"/api/users", "POST">
>;
```
This is what makes the safety real. It sits in a file the project's own `tsc` already compiles, so
`bun run typecheck` fails when a block sends a field the endpoint rejects. No bespoke type
comparison inside the WRNexus compiler, and no need to type-check build output. The language server
already runs TypeScript diagnostics on `.wrn` documents, so the same error appears inline.
**3. A generic runtime**, `callApi(path, method, input)`, typed by those contracts, so the fetch,
JSON handling, and failure branch live in one tested place instead of being re-emitted per block.
### Routes without a contract
A plain handler returning `Response.json` has no `defineEndpoint` contract, so `ApiInput` resolves
to `unknown`. The block's declared types are used directly and the generator emits a warning naming
the route. Untyped endpoints stay visible rather than silently passing.
### GET parameters travel as strings
A `GET` block's `parameters` become a query string (see Request assembly below), and every
`URLSearchParams` value is text on the wire regardless of the declared field type — a block
declaring `age?: number` still sends and receives `"30"`, not `30`. The declared type is honest
only because the endpoint's own schema coerces it back: `checkField` in
`packages/validation/src/index.ts` calls `Number(pre)` for every `v.number()` field — optional or
required — before the handler ever sees it, so `defineEndpoint({ input: v.object({ age:
v.number() }) })` invoked as `?age=30` hands the handler an actual `number` (verified end to end;
regression-tested in `packages/core/test/endpoint-schema.test.ts`, "a GET request coerces a
v.number() query param to an actual number"). This is a property of the endpoint's schema, not of
the `api` block or the generated contract types — a route that reads `ctx.url.searchParams`
directly, with no `defineEndpoint` schema, receives raw strings and gets no coercion, but that
route also has no contract for the generator to check against, so it already falls under "Routes
without a contract" above and is flagged there.
### Staleness
Checking is only as current as the generated file, so this stays wired into the existing
`check:generated-types` gate, which already verifies those artifacts match their sources.
## Compilation and runtime
### Client mode
Each `client { api name ... }` becomes an entry on an `api` namespace in the generated browser
module, beside the existing `__wrnexusClientFunctions`, with `const api = context.api` added to
client scope exactly as `server` is today.
Declared field types are **type-only**. The generator uses them for the `.d.ts` assertions and
codegen drops them before emit. A client function body that carried TypeScript into a `.mjs`
artifact is a bug this repository has already shipped once (fixed 2026-08-19, `55fed217`); the same
discipline applies here.
`setupCsrFetch` is untouched. Callable blocks are a separate mechanism, so the existing render-time
binding needs no rework.
### Request assembly
`callApi` builds the request:
- **GET** — declared `parameters` become a query string; `undefined` fields are omitted, which
removes the `if (filter.trim())` ladder authors write by hand.
- **Everything else** — a JSON body with `content-type: application/json`.
- Always `credentials: "same-origin"` and `accept: application/json`.
- **Non-GET requests attach `x-csrf-token`**, read from the `wrn-csrf` cookie or the
`wrnexus-csrf` meta tag, reusing the logic already at `packages/csr/src/reactive-runtime.ts:4221`
for RPC. Hand-written `fetch` calls in application code generally omit this, so it is a
correctness gain rather than only less typing.
### Failure
**`error {}` converts a failure into a value; without it, the call rejects.**
- 2xx — the JSON is parsed and bound as `data`, `response {}` runs, and its return value is the
call's result. With no `response` block, `data` is returned unchanged.
- Non-2xx, network failure, or an unparseable body — `error {}` runs with `status`, `message`, and
`data` in scope. `return []` yields an empty list and no exception.
- No `error {}` block — the promise rejects, so `try/catch` at the call site keeps working.
A block must never quietly return `undefined` on failure. Success-shaped failure is the defect class
this design is most concerned with, so the absence of an `error` block means throw, never swallow.
### The SSR boundary
`ssr {}` blocks accept `response {}` and `error {}`, but **not** `request {}`. There is no caller at
render time to supply arguments, and inferring an implicit source — page state, query parameters —
would be a guess. Parameterised requests are a client-mode feature; parameterised server-side
fetching stays with `server function`.
## Testing
### Parser (`packages/syntax/test/`)
- A sectioned block parses into `request`/`response`/`error` parts.
- A bare body still parses as the response block (the backward-compatibility guarantee).
- `request` inside an `ssr {}` block is a parse error naming the restriction.
- A malformed section reports the offending offset rather than failing later in codegen.
### Type generation (`packages/cli/test/` or the types generator's suite)
- A block targeting a `defineEndpoint` route emits an assertion referencing that contract.
- A field the endpoint does not accept makes `bun run typecheck` fail — asserted by running `tsc`
over a fixture, not by string-matching the generated file.
- A block targeting a contract-less route emits the warning and falls back to declared types.
- `check:generated-types` still passes with blocks present.
### Codegen (`packages/compiler/test/`)
- A client-mode block emits an `api` namespace entry and valid JavaScript — no TypeScript survives
into the browser module (the guard for the `55fed217` defect class).
- Declared types do not appear in the emitted module.
- An `ssr` block's output is unchanged from today for a bare body.
### Runtime (`packages/csr/test/`)
- GET omits `undefined` parameters and includes the rest.
- Non-GET attaches `x-csrf-token` from cookie and from meta.
- 2xx runs `response`; its return value is the result.
- Non-2xx runs `error`; its return value is the result.
- With no `error` block, a non-2xx rejects rather than resolving to `undefined`.
### End to end (`examples/basic-app`)
A page calling a typed block against a real route, driven in a browser: the request carries the
declared fields, the response block's value reaches page state, and a deliberately failing call
takes the `error` path. Tests that pass while the feature does not work have been a recurring
failure in this repository, so browser verification is part of the definition of done.
## Deferred
- External and third-party API targets, with the allowlist and credential handling they require.
- Author-settable request headers.
- Re-runnable `ssr` bindings — `setupCsrFetch`'s fetch-once guard stays as it is.
- Parameterised server-render fetching.
- Response caching and request de-duplication.
@@ -0,0 +1,140 @@
# Carrying projects to the new syntax with `wrnexus update` — Design
**Date:** 2026-08-19
**Status:** Approved for implementation
**Scope:** Migrations that take an existing project from today's syntax to the syntax left by the
cleanup and `apis { }` specs.
**Depends on:** `2026-08-19-legacy-and-config-cleanup-design.md` and
`2026-08-19-apis-block-design.md`. Both define the target this migrates to, so both must land first.
## Goal
After the two preceding specs, every existing project is written in a syntax the framework no longer
accepts. One `wrnexus update` should carry a project across — config keys removed, `api` blocks
moved into `apis { }`, mode-scoped helpers relocated — and, where it cannot do that safely, say so
precisely instead of guessing.
## What already exists
This is an extension of working machinery, not a new subsystem:
- `Migration { version, id, description, apply(ctx) }`, run when `from < version <= to`.
- `MigrationCtx` carries `appRoot`, `from`, `to`, **`dryRun`**, a report, and a logger.
- `MigrationReport` already separates `changedAutomatically`, `needsReview`, `parseFailures`,
`ambiguousFunctions`, and `unresolvedImports`.
- `update.ts` already imports `parse` and `formatWrn` from `@wrnexus/syntax`, so parsing a `.wrn`
file, transforming it, and re-emitting formatted source is an established pattern here.
The report's shape matters: it was built around the idea that some changes are safe to make and
others must be handed back to a human. That distinction is the backbone of this spec.
### Non-goals
- Migrating projects below `0.8.0`. The cleanup spec removes those migrations; no such project
exists.
- Rewriting application logic. Only the constructs these specs changed.
## The safety contract
**A file is transformed correctly, or it is left untouched and reported.** There is no third
outcome. Concretely:
1. Parse the file. A parse failure records the path in `parseFailures` and moves on — the file is
never partially rewritten.
2. Transform, then re-emit through `formatWrn`.
3. If any part of a file's transform cannot be completed, **the whole file is skipped** and recorded
in `needsReview` with the reason and the construct involved.
Running the migration twice must be a no-op: every transform detects already-migrated input and
does nothing. Dry-run must report exactly what a real run would change.
## The migrations
### 1. Remove the dead config keys
Delete `compatibility`, `functions`, `compatibilityDate`, and `frameworkBehaviour` from
`wrnexus.config.ts`. Mechanical, fully automatic, and safe because none of them was ever read.
### 2. `ssr { api … }` / `client { api … }``apis { … }`
Move each `api` entry into a page-level `apis { }` block, dropping the mode. Sectioned bodies —
those already using `request` / `response` / `error` — carry across unchanged, because the payload
is already bound to `data`.
Fully automatic. If a page has entries in both an `ssr` and a `client` block sharing a name, that is
a duplicate under the new rules and the **file is skipped and reported**, since choosing which one
survives is a decision about intent.
### 3. `ssr { functions { … } }``functions { shared function … }`
Relocate mode-scoped helpers to the page-level `functions { }` block with the `shared` modifier.
Automatic. If the page already has a function of the same name, the file is skipped and reported.
### 4. Legacy bare-body `api` blocks — **needs review, not automatic**
This is the one transform that cannot be done safely, and the spec is explicit about it rather than
attempting a best effort.
A legacy bare body is evaluated inside `with ($data ?? {})`, so it references payload fields as bare
identifiers:
```wrn
api ssrUsers GET /api/users/ssr {
return userNames(users)
}
```
The sectioned form binds the payload to `data`, so this must become `data.users`. But **which free
identifiers are payload fields is not knowable from the source.** In the example, `users` comes from
the response and `userNames` is a page helper — and nothing in the file distinguishes them. The
response shape belongs to the route, and the route may not even be typed.
A migration that guessed would produce code that compiles and is wrong: `data.userNames(...)` or an
untouched `users` that silently resolves to `undefined`. That is precisely the silent-wrong-answer
failure this project keeps paying for.
So: legacy bare-body blocks are **detected, reported in `needsReview` with the file, the block name,
and the free identifiers found**, and left untouched. The report tells the author exactly what to
decide. `wrnexus update` prints a short explanation of why this one is manual.
### 5. Deprecated `@wrnexus/auth` options
Only if the cleanup spec's optional auth section is included. Rename call sites of the superseded
options. Automatic where the rename is unambiguous; reported otherwise.
## Version
All of these attach to the release that ships the breaking change. After the cleanup spec the
migration floor is `0.8.0`, so the list is short and every entry is reachable.
## Output
At the end of a run the command prints, in this order: what it changed, what needs review and why,
and what failed to parse. A run with anything in `needsReview` or `parseFailures` exits non-zero, so
a scripted upgrade cannot appear to succeed while leaving a project half-migrated.
## Testing
Each migration gets a fixture project and three assertions: the transform produces the expected
source, running it a second time changes nothing, and a dry run reports the same set without writing.
- **Config removal** — keys gone, rest of the config untouched.
- **`api` relocation** — a page with both `ssr` and `client` api blocks lands in one `apis { }`;
entries keep their names, methods, paths, and sections.
- **Name collision across modes** — the file is skipped and reported, not silently merged.
- **Mode functions** — relocated with the `shared` modifier; a name collision skips and reports.
- **Legacy bare body** — reported in `needsReview` with the block name and free identifiers, and the
file is byte-identical afterwards. This is the most important test in the spec: it pins that the
migration does _not_ attempt the rewrite.
- **Parse failure** — a malformed `.wrn` is recorded in `parseFailures` and left untouched.
- **Exit code** — non-zero when anything needs review.
- **End to end**`examples/basic-app` migrated by the command alone, then built and tested. If the
framework's own example cannot be migrated by the tool, the tool is not finished.
## What this does not promise
Automated source rewriting cannot be promised as "perfect". What is promised is bounded: every file
is either correctly transformed or untouched and named in the report, with the reason. Nothing is
half-rewritten, and nothing is guessed. The legacy bare-body case is deliberately manual because a
correct automatic answer does not exist.
+31 -31
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": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
"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"
+28 -2
View File
@@ -1,5 +1,31 @@
# 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
- Added HTML tag and attribute completions inside WRN `view` blocks, while preserving WRNexus
component completion priority and suppressing HTML suggestions outside markup regions.
- Added HTML hover documentation, folding ranges, linked tag editing, and automatic closing tags.
- Added Emmet expansion support for WRN documents and kept void elements from receiving closing tags.
- Hardened completion and auto-close handling against quoted attribute values, replaced selections,
stale asynchronous edits, and duplicate client-side suggestions.
## 0.8.3 ## 0.8.3
- Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes. - Rebuilt the embedded WRN compiler with the 0.8.3 SSR, computed-value, Async-scope, and typed loop-prop fixes.
@@ -16,8 +42,8 @@
- Kept component prop/event intelligence active while the shared language server is enabled. - Kept component prop/event intelligence active while the shared language server is enabled.
- Suppressed unavailable TypeScript standard-library and unmapped virtual-document implementation - Suppressed unavailable TypeScript standard-library and unmapped virtual-document implementation
diagnostics in packaged extension environments. diagnostics in packaged extension environments.
- Fixed false `unknown`/index-access diagnostics in valid dynamic event forwarding handlers such as - Fixed false `unknown`/index-access diagnostics in valid dynamic event forwarding handlers that
`output[type](payload)` by preserving JavaScript semantics for omitted parameter types. dispatch a payload by event type, preserving JavaScript semantics for omitted parameter types.
- Resolved TypeScript standard libraries from the active workspace so semantic diagnostics run - Resolved TypeScript standard libraries from the active workspace so semantic diagnostics run
consistently in the repository and extension development environment. consistently in the repository and extension development environment.
+13 -4
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",
@@ -134,6 +134,11 @@
"maximum": 240, "maximum": 240,
"scope": "resource", "scope": "resource",
"description": "Preferred WRNexus formatter line width before long tags are expanded." "description": "Preferred WRNexus formatter line width before long tags are expanded."
},
"wrnexus.html.autoClosingTags": {
"type": "boolean",
"default": true,
"description": "Automatically close HTML tags inside .wrn view blocks."
} }
} }
}, },
@@ -141,6 +146,9 @@
"files.associations": { "files.associations": {
"*.wrn": "wrn" "*.wrn": "wrn"
}, },
"emmet.includeLanguages": {
"wrn": "html"
},
"[wrn]": { "[wrn]": {
"editor.defaultFormatter": "wrnexus.wrnexus", "editor.defaultFormatter": "wrnexus.wrnexus",
"editor.formatOnSave": false, "editor.formatOnSave": false,
@@ -276,13 +284,14 @@
"check": "bun run build && bun run test && bun run validate", "check": "bun run build && bun run test && bun run validate",
"vscode:prepublish": "bun run check", "vscode:prepublish": "bun run check",
"package": "vsce package --no-dependencies --no-rewrite-relative-links", "package": "vsce package --no-dependencies --no-rewrite-relative-links",
"publish": "vsce publish --no-dependencies --no-rewrite-relative-links", "publish": "vsce publish --no-dependencies",
"publish:azure": "vsce publish --no-dependencies --no-rewrite-relative-links --azure-credential" "publish:azure": "vsce publish --no-dependencies --azure-credential"
}, },
"devDependencies": { "devDependencies": {
"@vscode/vsce": "^3.9.2" "@vscode/vsce": "^3.9.2"
}, },
"dependencies": { "dependencies": {
"vscode-languageclient": "^10.1.0" "vscode-languageclient": "^10.1.0",
"vscode-html-languageservice": "^5.6.2"
} }
} }
+83
View File
@@ -0,0 +1,83 @@
"use strict";
const vscode = require("vscode");
/**
* Auto-close tags as they are typed.
*
* LSP has no request for this, so the client watches document changes and asks
* the server whether the tag should close. The server owns the decision because
* void elements and already-closed tags must not be closed.
*/
function registerAutoCloseTags(context, client) {
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
if (event.document.languageId !== "wrn") return;
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
const changes = event.contentChanges;
if (!changes.length) return;
const typed = changes[0].text;
if (typed !== ">" && typed !== "/") return;
// Every cursor must have typed the same trigger. A replaced selection
// (overtype, or select-and-type) is declined rather than guessed at.
if (!changes.every((change) => change.text === typed && change.rangeLength === 0)) return;
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document !== event.document) return;
/*
* Positions come from the editor's selections, not from the changes.
*
* A change's `range` is in coordinates from before the whole event, so with
* several cursors on one line every range after the first is short by the
* insertions preceding it. The selections have already been adjusted for
* the edit, so they are where the carets actually are.
*/
const positions = editor.selections.map((selection) => selection.active);
if (positions.length !== changes.length) return;
if (!editor.selections.every((selection) => selection.isEmpty)) return;
const documentVersion = event.document.version;
const snippets = await Promise.all(
positions.map((position) =>
client.sendRequest("wrn/tagComplete", {
textDocument: { uri: event.document.uri.toString() },
position: { line: position.line, character: position.character },
}),
),
);
if (!snippets.every((snippet) => typeof snippet === "string" && snippet)) return;
/*
* One insertSnippet call carries one snippet, and it is the only form that
* keeps every caret: inserting sequentially would collapse the selection to
* the first snippet and invalidate the remaining positions. Cursors that
* want different closing tags are therefore declined rather than
* half-applied -- multi-cursor editing of matching lines, which is what
* this is for, produces one snippet for all of them.
*/
if (!snippets.every((snippet) => snippet === snippets[0])) return;
// The user may have kept typing during the round-trip; re-validate everything the
// insertion depends on before touching the document, since a stale offset would
// silently corrupt it.
if (vscode.window.activeTextEditor !== editor) return;
if (editor.document !== event.document) return;
if (editor.document.version !== documentVersion) return;
if (editor.selections.length !== positions.length) return;
if (
!editor.selections.every(
(selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]),
)
) {
return;
}
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
});
context.subscriptions.push(listener);
}
module.exports = { registerAutoCloseTags };
File diff suppressed because it is too large Load Diff
+86
View File
@@ -306,6 +306,54 @@ const BLOCK_COMPLETIONS = [
"}", "}",
].join("\n"), ].join("\n"),
}, },
{
label: "apis",
detail: "Page-level API call declarations block",
documentation:
"Declare the API calls available to this page as named entries, each callable via api.<name>() from functions and the `api=` view binding.",
snippet: [
"apis {",
" ${1:searchUsers} ${2|GET,POST,PUT,PATCH,DELETE|} ${3:/api/users} {",
" request {",
" body {",
" ${4:name}?: ${5:string}",
" }",
" }",
"",
" response {",
" return ${6:data}",
" }",
"",
" error {",
" return ${7:null}",
" }",
" }",
"}",
].join("\n"),
},
{
label: "apis entry",
detail: "Single API call declaration",
documentation:
"Declare a single named API call inside an apis { } block: <name> <METHOD> <path> { request/response/error }.",
snippet: [
"${1:searchUsers} ${2|GET,POST,PUT,PATCH,DELETE|} ${3:/api/users} {",
" request {",
" body {",
" ${4:name}?: ${5:string}",
" }",
" }",
"",
" response {",
" return ${6:data}",
" }",
"",
" error {",
" return ${7:null}",
" }",
"}",
].join("\n"),
},
{ {
label: "persist", label: "persist",
detail: "Include-only store persistence", detail: "Include-only store persistence",
@@ -570,6 +618,41 @@ function isInsideWatch(document, position) {
return depth > 0; return depth > 0;
} }
/**
* Whether an offset sits inside a `view { }` block.
*
* The language server owns completion there and returns a merged list, so this
* provider stands down to avoid VS Code concatenating two independent lists.
* Quotes are only tracked inside a tag: `<p>it's</p>` would otherwise open a
* string that never closes.
*/
function isInsideViewBlock(text, offset) {
const pattern = /\bview\s*\{/g;
let match;
while ((match = pattern.exec(text))) {
const start = match.index + match[0].length;
let depth = 1;
let inTag = false;
let quote = null;
let index = start;
for (; index < text.length && depth > 0; index += 1) {
const char = text[index];
if (quote) {
if (char === quote) quote = null;
continue;
}
if (inTag && (char === '"' || char === "'")) quote = char;
else if (char === "<") inTag = true;
else if (char === ">") inTag = false;
else if (char === "{") depth += 1;
else if (char === "}") depth -= 1;
}
if (offset >= start && offset <= index) return true;
pattern.lastIndex = index;
}
return false;
}
function isAfterWatchKeyword(document, position) { function isAfterWatchKeyword(document, position) {
const linePrefix = document.lineAt(position.line).text.slice(0, position.character); const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
@@ -634,6 +717,8 @@ function addFunctionCompletions(items, document) {
} }
function provideCompletionItems(document, position) { function provideCompletionItems(document, position) {
if (isInsideViewBlock(document.getText(), document.offsetAt(position))) return [];
const items = []; const items = [];
const linePrefix = document.lineAt(position.line).text.slice(0, position.character); const linePrefix = document.lineAt(position.line).text.slice(0, position.character);
@@ -707,6 +792,7 @@ module.exports = {
extractProps, extractProps,
extractRouteParams, extractRouteParams,
extractStates, extractStates,
isInsideViewBlock,
provideCompletionItems, provideCompletionItems,
registerCompletionProvider, registerCompletionProvider,
}; };
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -4,6 +4,7 @@
const path = require("node:path"); const path = require("node:path");
const vscode = require("vscode"); const vscode = require("vscode");
const { LanguageClient, TransportKind } = require("vscode-languageclient/node"); const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
const { registerAutoCloseTags } = require("./auto-close-tags.js");
const WRN_LANGUAGE_ID = "wrn"; const WRN_LANGUAGE_ID = "wrn";
/** @type {LanguageClient | undefined} */ /** @type {LanguageClient | undefined} */
@@ -43,6 +44,7 @@ async function activate(context) {
{ documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }] }, { documentSelector: [{ scheme: "file", language: WRN_LANGUAGE_ID }] },
); );
await client.start(); await client.start();
registerAutoCloseTags(context, client);
} }
async function deactivate() { async function deactivate() {
@@ -51,4 +53,4 @@ async function deactivate() {
if (running) await running.stop(); if (running) await running.stop();
} }
module.exports = { activate, deactivate, recoverWrnLanguage }; module.exports = { activate, deactivate, recoverWrnLanguage, registerAutoCloseTags };
File diff suppressed because one or more lines are too long
+37 -15
View File
@@ -113,10 +113,10 @@
"include": "#action-block" "include": "#action-block"
}, },
{ {
"include": "#mode-block" "include": "#api-block"
}, },
{ {
"include": "#api-block" "include": "#apis-block"
}, },
{ {
"include": "#functions-block" "include": "#functions-block"
@@ -513,8 +513,8 @@
} }
] ]
}, },
"mode-block": { "apis-block": {
"begin": "\\b(ssr|client)\\b\\s*(\\{)", "begin": "\\b(apis)\\b\\s*(\\{)",
"beginCaptures": { "beginCaptures": {
"1": { "1": {
"name": "keyword.control.wrn" "name": "keyword.control.wrn"
@@ -534,21 +534,18 @@
"include": "#comments" "include": "#comments"
}, },
{ {
"begin": "\\b(api)\\b\\s+([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)", "begin": "\\b([A-Za-z_$][A-Za-z0-9_$]*)\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\\b\\s*([^\\s{]*)\\s*(\\{)",
"beginCaptures": { "beginCaptures": {
"1": { "1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "entity.name.function.wrn" "name": "entity.name.function.wrn"
}, },
"3": { "2": {
"name": "constant.language.http-method.wrn" "name": "constant.language.http-method.wrn"
}, },
"4": { "3": {
"name": "string.unquoted.route.wrn" "name": "string.unquoted.route.wrn"
}, },
"5": { "4": {
"name": "punctuation.definition.block.begin.wrn" "name": "punctuation.definition.block.begin.wrn"
} }
}, },
@@ -558,8 +555,36 @@
"name": "punctuation.definition.block.end.wrn" "name": "punctuation.definition.block.end.wrn"
} }
}, },
"contentName": "meta.embedded.block.ts",
"patterns": [ "patterns": [
{
"include": "#comments"
},
{
"begin": "\\b(request|response|error)\\b\\s*(\\{)",
"beginCaptures": {
"1": {
"name": "keyword.control.wrn"
},
"2": {
"name": "punctuation.definition.block.begin.wrn"
}
},
"end": "\\}",
"endCaptures": {
"0": {
"name": "punctuation.definition.block.end.wrn"
}
},
"contentName": "meta.embedded.block.ts",
"patterns": [
{
"include": "#ts-braces"
},
{
"include": "source.ts"
}
]
},
{ {
"include": "#ts-braces" "include": "#ts-braces"
}, },
@@ -567,9 +592,6 @@
"include": "source.ts" "include": "source.ts"
} }
] ]
},
{
"include": "#functions-block"
} }
] ]
}, },
+178
View File
@@ -0,0 +1,178 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { installVsCodeHost } = require("./vscode-host.js");
class Position {
constructor(line, character) {
this.line = line;
this.character = character;
}
translate(lineDelta, characterDelta) {
return new Position(this.line + lineDelta, this.character + characterDelta);
}
isEqual(other) {
return this.line === other.line && this.character === other.character;
}
}
class Selection {
constructor(active) {
this.active = active;
this.anchor = active;
this.isEmpty = true;
}
}
class SnippetString {
constructor(value) {
this.value = value;
}
}
let changeListener = null;
const host = {
Position,
Selection,
SnippetString,
workspace: {
onDidChangeTextDocument(listener) {
changeListener = listener;
return { dispose() {} };
},
getConfiguration() {
return { get: (_key, fallback) => fallback };
},
},
window: { activeTextEditor: null },
};
const restoreHost = installVsCodeHost(host);
const { registerAutoCloseTags } = require("../src/auto-close-tags.js");
restoreHost();
/**
* Drive the handler the way VS Code does: the document has already been
* updated and the carets moved by the time the change event fires.
*/
function scenario({ carets, snippetFor, typed = ">" }) {
const inserted = [];
const asked = [];
const document = { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } };
const editor = {
document,
selections: carets.map((caret) => new Selection(caret)),
insertSnippet(snippet, positions) {
inserted.push({ value: snippet.value, positions });
return Promise.resolve(true);
},
};
editor.selection = editor.selections[0];
host.window.activeTextEditor = editor;
const client = {
sendRequest(_method, params) {
asked.push(params.position);
return Promise.resolve(snippetFor(params.position));
},
};
registerAutoCloseTags({ subscriptions: [] }, client);
return {
inserted,
asked,
fire: () =>
changeListener({
document,
// Pre-edit coordinates, deliberately not usable as caret positions.
contentChanges: carets.map(() => ({
text: typed,
rangeLength: 0,
range: { start: new Position(0, 0) },
})),
}),
};
}
test("closes the tag at a single caret", async () => {
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
await run.fire();
assert.equal(run.inserted.length, 1);
assert.equal(run.inserted[0].value, "$0</div>");
assert.deepEqual(
run.inserted[0].positions.map((p) => [p.line, p.character]),
[[1, 8]],
);
});
test("closes the tag at every caret in one insertion", async () => {
// One insertSnippet call is what keeps all the carets alive: inserting
// sequentially would collapse the selection to the first snippet.
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8), new Position(3, 8)],
snippetFor: () => "$0</div>",
});
await run.fire();
assert.equal(run.asked.length, 3);
assert.equal(run.inserted.length, 1);
assert.deepEqual(
run.inserted[0].positions.map((p) => [p.line, p.character]),
[
[1, 8],
[2, 8],
[3, 8],
],
);
});
test("asks about each caret's own position rather than the change ranges", async () => {
// Every contentChange above reports (0, 0). Using those would query and
// insert at the wrong offsets once more than one caret is on a line.
const run = scenario({
carets: [new Position(4, 12), new Position(9, 3)],
snippetFor: () => "$0</p>",
});
await run.fire();
assert.deepEqual(
run.asked.map((p) => [p.line, p.character]),
[
[4, 12],
[9, 3],
],
);
});
test("declines when the carets want different closing tags", async () => {
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8)],
snippetFor: (position) => (position.line === 1 ? "$0</div>" : "$0</span>"),
});
await run.fire();
assert.equal(run.inserted.length, 0);
});
test("declines when any caret has no tag to close", async () => {
const run = scenario({
carets: [new Position(1, 8), new Position(2, 8)],
snippetFor: (position) => (position.line === 1 ? "$0</br>" : null),
});
await run.fire();
assert.equal(run.inserted.length, 0);
});
test("declines a replaced selection", async () => {
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
await changeListener({
document: { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } },
contentChanges: [{ text: ">", rangeLength: 3, range: { start: new Position(1, 5) } }],
});
assert.equal(run.inserted.length, 0);
});
@@ -0,0 +1,103 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert");
const { installVsCodeHost } = require("./vscode-host.js");
const restoreHost = installVsCodeHost({
Position: class Position {
constructor(line, character) {
this.line = line;
this.character = character;
}
},
Range: class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
},
CompletionItem: class CompletionItem {
constructor(label, kind) {
this.label = label;
this.kind = kind;
}
},
CompletionItemKind: {
Event: 23,
Property: 10,
Function: 12,
Keyword: 14,
Variable: 13,
},
SnippetString: class SnippetString {
constructor(text) {
this.value = text;
}
},
MarkdownString: class MarkdownString {
constructor(text) {
this.value = text;
}
},
});
const { isInsideViewBlock, provideCompletionItems } = require("../src/completion.js");
restoreHost();
const PAGE = `page Home {
view {
<div>hello</div>
}
functions {
function go() {}
}
}
`;
test("a markup offset is inside a view block", () => {
assert.equal(isInsideViewBlock(PAGE, PAGE.indexOf("<div")), true);
});
test("a functions-block offset is not inside a view block", () => {
assert.equal(isInsideViewBlock(PAGE, PAGE.indexOf("function go")), false);
});
test("provideCompletionItems returns empty array when inside view block", () => {
const document = {
getText() {
return PAGE;
},
offsetAt() {
return PAGE.indexOf("<div");
},
lineAt() {
return { text: "<div>hello</div>" };
},
fileName: "test.wrn",
};
const position = { line: 2, character: 4 };
const result = provideCompletionItems(document, position);
assert.equal(Array.isArray(result), true);
assert.equal(result.length, 0);
});
test("provideCompletionItems returns non-empty array when inside functions block", () => {
const document = {
getText() {
return PAGE;
},
offsetAt() {
return PAGE.indexOf("function go");
},
lineAt() {
return { text: " function go() {}" };
},
fileName: "test.wrn",
};
const position = { line: 5, character: 4 };
const result = provideCompletionItems(document, position);
assert.equal(Array.isArray(result), true);
assert(result.length > 0, "should return non-empty completions outside view block");
});
+3 -7
View File
@@ -2,17 +2,13 @@
const assert = require("node:assert"); const assert = require("node:assert");
const { test } = require("node:test"); const { test } = require("node:test");
const Module = require("node:module"); const { installVsCodeHost } = require("./vscode-host.js");
// These extraction helpers are pure, but their module also registers VS Code // These extraction helpers are pure, but their module also registers VS Code
// providers at runtime. Supply a minimal host shim for unit tests. // providers at runtime. Supply a minimal host shim for unit tests.
const originalLoad = Module._load; const restoreHost = installVsCodeHost({});
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return {};
return originalLoad.call(this, request, parent, isMain);
};
const { extractRouteParams, extractStates } = require("../src/completion"); const { extractRouteParams, extractStates } = require("../src/completion");
Module._load = originalLoad; restoreHost();
test("extracts dynamic route params from filename", () => { test("extracts dynamic route params from filename", () => {
const document = { const document = {
+18 -24
View File
@@ -2,30 +2,24 @@
const assert = require("node:assert"); const assert = require("node:assert");
const { test } = require("node:test"); const { test } = require("node:test");
const Module = require("node:module"); const { installVsCodeHost } = require("./vscode-host.js");
const originalLoad = Module._load; const restoreHost = installVsCodeHost({
Module._load = function load(request, parent, isMain) { Diagnostic: class Diagnostic {
if (request === "vscode") { constructor(range, message, severity) {
return { this.range = range;
Diagnostic: class Diagnostic { this.message = message;
constructor(range, message, severity) { this.severity = severity;
this.range = range; }
this.message = message; },
this.severity = severity; DiagnosticSeverity: { Error: 0, Warning: 1 },
} Range: class Range {
}, constructor(start, end) {
DiagnosticSeverity: { Error: 0, Warning: 1 }, this.start = start;
Range: class Range { this.end = end;
constructor(start, end) { }
this.start = start; },
this.end = end; });
}
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
const { const {
findTopLevelDeclaration, findTopLevelDeclaration,
maskLeadingTrivia, maskLeadingTrivia,
@@ -34,7 +28,7 @@ const {
validateLayoutUsage, validateLayoutUsage,
validateRootMembers, validateRootMembers,
} = require("../src/diagnostics"); } = require("../src/diagnostics");
Module._load = originalLoad; restoreHost();
function mockDocument() { function mockDocument() {
return { return {
+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"}]
+133
View File
@@ -0,0 +1,133 @@
"use strict";
const assert = require("node:assert");
const { test } = require("node:test");
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const grammarPath = join(__dirname, "../syntaxes/wrn.tmLanguage.json");
const grammar = JSON.parse(readFileSync(grammarPath, "utf8"));
test("the grammar knows the apis block", () => {
assert.ok(grammar.repository["apis-block"], "apis should appear as a block keyword");
});
test("client keeps its highlighting where it is still valid", () => {
// client state {}, runtime = "client", and client function all survive.
// Only the client {} data block was removed.
assert.match(JSON.stringify(grammar), /client/, "client must still be matched");
assert.match(
JSON.stringify(grammar),
/shared/,
"the shared function modifier must still be matched",
);
});
// --- Structural assertions: these fail if the rule they name is deleted, ---
// --- even though the substrings "apis"/"client" remain elsewhere in the ---
// --- file (e.g. inside unrelated pattern names or comments). ---
/**
* vscode-textmate is not a dependency of this repo (confirmed via
* `require.resolve`), so real tokenization is unavailable here. Instead we
* pull each rule's own `begin`/`match` regex out of the parsed grammar
* object and exercise it directly with the JS regex engine. The grammar's
* patterns use only `\b`, character classes, and `(?<=...)` lookbehind --
* all supported by native JS regexes -- so this is a faithful proxy for
* "does this specific rule match this specific source line" without
* needing an Oniguruma-backed tokenizer.
*/
function ruleByTopLevelBlockName(name) {
const entry = grammar.repository[name];
assert.ok(entry, `repository.${name} should exist`);
return entry;
}
function topLevelBlockIncludes(name) {
return grammar.repository.blocks.patterns.some((p) => p.include === `#${name}`);
}
test("blocks includes the apis-block pattern", () => {
assert.ok(topLevelBlockIncludes("apis-block"), "#blocks must include #apis-block");
});
test("the apis-block begin pattern matches the container keyword", () => {
const rule = ruleByTopLevelBlockName("apis-block");
const beginRe = new RegExp(rule.begin);
assert.ok(beginRe.test("apis {"), "apis-block begin should match 'apis {'");
assert.ok(!beginRe.test("ssr {"), "apis-block begin must not match 'ssr {'");
assert.ok(!beginRe.test("client {"), "apis-block begin must not match 'client {'");
});
test("the apis-block declares an entry pattern for <name> <METHOD> <path>", () => {
const rule = ruleByTopLevelBlockName("apis-block");
const entryPattern = (rule.patterns || []).find(
(p) => p.name === "apis-entry" || (p.begin && /GET\|POST/.test(p.begin)),
);
assert.ok(entryPattern, "apis-block should contain an entry declaration pattern");
const entryRe = new RegExp(entryPattern.begin || entryPattern.match);
assert.ok(
entryRe.test("searchUsers POST /api/users {"),
"the entry pattern should match '<name> <METHOD> <path> {'",
);
});
test("the ssr/client data-block pattern (mode-block) is gone", () => {
assert.strictEqual(
grammar.repository["mode-block"],
undefined,
"mode-block (the removed ssr {}/client {} data-block rule) must be deleted from the repository",
);
assert.ok(
!topLevelBlockIncludes("mode-block"),
"#blocks must no longer include the removed #mode-block pattern",
);
});
// --- Regression coverage: constructs that must NOT be broken by this change ---
test("REGRESSION: client state {} still highlights via grouped-state-block", () => {
const rule = ruleByTopLevelBlockName("grouped-state-block");
const beginRe = new RegExp(rule.begin);
assert.ok(
beginRe.test("client state {"),
"grouped-state-block must still match 'client state {'",
);
});
test('REGRESSION: runtime = "client" still highlights via execution-decl', () => {
const rule = ruleByTopLevelBlockName("execution-decl");
const beginRe = new RegExp(rule.begin);
assert.ok(
beginRe.test('runtime = "client"'),
"execution-decl must still match 'runtime = \"client\"'",
);
});
test("REGRESSION: the client function modifier still highlights via functions-block", () => {
const rule = ruleByTopLevelBlockName("functions-block");
const matchRe = new RegExp(
rule.patterns.find(
(p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn",
).match,
);
assert.ok(
matchRe.test("client async function go(): Promise<void> {"),
"functions-block must still recognize the 'client' runtime modifier on a function",
);
});
test("REGRESSION: the client function modifier also highlights via v060-keywords", () => {
const rule = ruleByTopLevelBlockName("v060-keywords");
const modifierPattern = rule.patterns.find((p) => p.name === "storage.modifier.runtime.wrn");
assert.ok(modifierPattern, "v060-keywords should have a storage.modifier.runtime.wrn rule");
const matchRe = new RegExp(modifierPattern.match);
assert.ok(
matchRe.test("client function go"),
"v060-keywords must still recognize 'client' before 'function'",
);
assert.ok(
matchRe.test("client state"),
"v060-keywords must still recognize 'client' before 'state'",
);
});
+15
View File
@@ -113,6 +113,21 @@ try {
readFileSync(join(root, rel), "utf8"); readFileSync(join(root, rel), "utf8");
ok(`Marketplace document exists: ${rel}`); ok(`Marketplace document exists: ${rel}`);
} }
const emmetLanguages = manifest.contributes?.configurationDefaults?.["emmet.includeLanguages"];
emmetLanguages?.wrn === "html"
? ok("Emmet is mapped for wrn documents")
: bad("Emmet is mapped for wrn documents", `got ${JSON.stringify(emmetLanguages)}`);
const autoClose =
manifest.contributes?.configuration?.properties?.["wrnexus.html.autoClosingTags"];
autoClose?.type === "boolean" && autoClose?.default === true
? ok("auto-closing tags setting is contributed")
: bad("auto-closing tags setting is contributed", `got ${JSON.stringify(autoClose)}`);
manifest.dependencies?.["vscode-html-languageservice"]
? ok("HTML language service ships as a runtime dependency")
: bad("HTML language service ships as a runtime dependency");
} catch (e) { } catch (e) {
bad("Marketplace metadata", e.message); bad("Marketplace metadata", e.message);
} }
+39
View File
@@ -0,0 +1,39 @@
"use strict";
/**
* Supply a stub `vscode` host so extension sources can be unit tested.
*
* These files run under `node --test` (see the package's test script), where
* patching `Module._load` is enough. A bare `bun test` from the repository
* root also picks them up by filename, and Bun resolves `require` through its
* own resolver without consulting `Module._load` -- so under Bun the same
* files failed with "Cannot find package 'vscode'". Registering a virtual
* module covers that case, leaving one shim that works under both runners.
*
* Returns a function restoring the original loader.
*/
function installVsCodeHost(stub) {
const Module = require("node:module");
if (typeof Bun !== "undefined") {
require("bun").plugin({
name: "vscode-host-stub",
setup(build) {
build.module("vscode", () => ({ exports: stub, loader: "object" }));
},
});
return () => {};
}
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "vscode") return stub;
return originalLoad.call(this, request, parent, isMain);
};
return () => {
Module._load = originalLoad;
};
}
module.exports = { installVsCodeHost };
+295
View File
@@ -0,0 +1,295 @@
# WrNexus app - instructions for AI coding assistants
This is a **WrNexus** app. When creating or editing pages, components, API routes,
or features, follow the framework conventions below. WrNexus is private and not in
your training data, so rely on these rules - do NOT assume React/Next.js/Vue patterns.
# WrNexus
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
> This document teaches an AI how to write correct WrNexus code. It is private and
> post-dates model training data, so rely on THIS document, not prior web-framework
> assumptions.
## Golden rules
- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
- **Routing is file-based** under `app/`. The filename is the route. No router config.
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
on the server and hydrate automatically — you never write client-side JS islands.
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.
## Project layout
```
app/
pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
components/ *.wrn → reusable UI, mounted in a page/component via <div data-component="name" ...props>
layouts/ *.wrn → named layouts; a page opts in with layout = "name"
api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
middleware/ *.ts → export default async (ctx, next) => next()
realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/<name>)
schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody
locales/ *.json → i18n messages per language
db/ schema.ts, queries/*.sql, migrations/*.sql
styles/ global.css → Tailwind (default) or plain CSS
wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles")
public/ → static assets served at /
```
## `.wrn` page
```wrn
page Home {
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
seo {
title = "Home"
description = "..."
canonical = "/"
}
view {
<h1>Hello</h1>
<p>Count is {count}, doubled is {count * 2}.</p>
<button @click="count++">Increment</button>
<div data-component="counter" start="5" label="Clicks"></div>
}
style {
h1 { color: var(--wrn-color-text); }
}
}
```
## `.wrn` component
```wrn
component Counter {
props { // props come from mount attributes; each is coerced to the
start = 0 // TYPE of its default (so start="5" arrives as the number 5)
label = "Count"
}
state count = start // state may reference props
view {
<button @click="count++">{label}: {count}</button>
}
}
```
Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
Components render on the server with their props, then hydrate — no per-component JS.
## The `view { }` block (plain HTML + a few directives)
- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
- Theme: any element with `data-wrn-theme-toggle` toggles light/dark; `data-wrn-theme-set="dark"` sets it.
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.
## Data-driven tables / lists (server-rendered `.wrn`)
Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
This renders on the **server** (SSR-first) and is HTML-escaped by default.
```wrn
page Admin {
layout = "dashboard"
// Fetch on the server. The api handler at /api/contacts returns { contacts: [...] };
// this block's `return contacts` exposes that array (via `$data`) as the binding `rows`.
ssr {
api rows GET /api/contacts { return contacts }
}
view {
<table>
<tbody>
{#each rows as r, i}
<tr>
<td>#{i}</td>
<td>{r.name}</td>
<td><a href="mailto:{r.email}">{r.email}</a></td>
</tr>
{:empty}
<tr><td colspan="3">No submissions yet.</td></tr>
{/each}
</tbody>
</table>
}
}
```
The matching API returns the array under a key the `ssr` block reads:
```ts
// app/api/contacts.ts → GET /api/contacts
import { getDb } from "@wrnexus/db";
export const GET = async () => {
const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC");
return Response.json({ contacts }); // ssr block does `return contacts`
};
```
**Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx`
pages returning an HTML string are also supported for fully-custom programmatic rendering,
but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.)
## API routes (`app/api/*.ts`)
```ts
// app/api/users/list.ts → GET /api/users/list
import { getDb } from "@wrnexus/db";
export const GET = async (ctx) => {
return Response.json({ users: await ListUsers(getDb()) });
};
export const POST = async (ctx) => {
const body = await ctx.req.json();
return Response.json({ ok: true, body }, { status: 201 });
};
```
`ctx` (the `Context` from `@wrnexus/core`) has:
`req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]``ctx.params.id`),
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
`@wrnexus/helpers` instead of constructing it from untrusted headers:
```ts
import { redirectToLogin } from "@wrnexus/helpers";
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
```
The package also exports `getOriginalRequestUrl`, `getOriginalRequestOrigin`,
`getOriginalRequestPath`, and `getOriginalRequestMethod`. Always pass `allowedHosts` when
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
## Middleware & realtime
```ts
// app/middleware/logger.ts
export default async function logger(ctx, next) {
console.log(ctx.req.method, ctx.url.pathname);
return next(); // return a Response WITHOUT calling next() to short-circuit
}
```
```ts
// app/realtime/chat.ts → ws://host/realtime/chat
import { defineRoom } from "@wrnexus/core";
export default defineRoom({
onConnect(client) {
client.send({ type: "system", text: "connected" });
},
onMessage(client, msg) {
client.room.broadcast({ type: "message", data: msg });
},
});
```
Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime).
## Config (`wrnexus.config.ts`)
```ts
import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = {
seo: { title: "App", titleTemplate: "%s | App", description: "..." },
styles: {
entry: "app/styles/global.css",
process: async ({ entryPath, mode }) => /* Tailwind */ "",
},
fonts: {
sans: '"Inter", system-ui, sans-serif',
google: [{ family: "Inter", weights: [400, 600] }],
},
theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } },
i18n: { default: "en", locales: ["en", "es"] },
db: { driver: "sqlite", url: "file:./dev.db" },
security: { cors: { enabled: true, origin: ["http://localhost:5173"] } },
// profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } },
};
export default config;
```
## Database (`@wrnexus/db`)
```ts
// app/db/schema.ts
import { v, table } from "@wrnexus/db";
export const users = table("users", {
id: v.id(),
name: v.string(),
email: v.string().unique(),
createdAt: v.timestamp(),
});
```
- Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions.
- Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());`
- Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite).
## Validation (`@wrnexus/validation`)
```ts
// app/schemas/login.ts
import { v } from "@wrnexus/validation";
export default v.object({
email: v.string().email(),
password: v.string().min(8),
});
```
In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);``r.ok ? r.value : r.response`.
In a form: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (client + server validation connected automatically).
## AI / LLM (`@wrnexus/ai`)
```ts
// app/api/ai.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI(); // reads ANTHROPIC_API_KEY; default model claude-opus-4-8
export const POST = async (ctx) => {
const { prompt } = await ctx.req.json();
return ai.streamResponse(prompt); // or: return Response.json({ text: await ai.generate(prompt) })
};
```
## CLI
```
wrnexus dev . # dev server + HMR
wrnexus build . # production build → dist/server.js
bun dist/server.js # run the production server (or npm start)
wrnexus create <name> # scaffold a new app
wrnexus update --latest # deps + syntax/config migrations + verification
wrnexus generate page <Name> # scaffold a page (aliases: g p)
wrnexus generate component <name> | api <path> | schema <name>
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
wrnexus eject <component> # copy a WrNexus UI component's .wrn into app/components to customize
```
## When asked to "create a page/component/feature"
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wrn-*)`), or `style { }`.
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.
+27
View File
@@ -0,0 +1,27 @@
import { defineEndpoint } from "@wrnexus/core";
import { SearchDirectorySchema } from "../schemas/search-directory.ts";
interface DirectoryUser {
name: string;
designation: string;
}
const ALL: DirectoryUser[] = [
{ name: "Ajay", designation: "UI" },
{ name: "Asha", designation: "Backend" },
{ name: "Chen", designation: "UI" },
];
/**
* Schema-typed handler: input resolves to `{ name?: string }` from
* SearchDirectorySchema, so the block's request shape is checked for real
* (not against `unknown`, which the untyped-handler shape resolved to).
*/
export const POST = defineEndpoint<{ name?: string }, { users: DirectoryUser[] }>({
input: SearchDirectorySchema,
description: "Case-insensitive substring search over the demo directory by name.",
handler(input) {
const needle = String(input.name ?? "").toLowerCase();
return { users: ALL.filter((user) => user.name.toLowerCase().includes(needle)) };
},
});
+1 -1
View File
@@ -1,4 +1,4 @@
// AUTO-GENERATED by `wrnexus db generate` — do not edit. // AUTO-GENERATED by `wrnexus db generate` (dialect: sqlite) — do not edit.
import type { Db, ExecResult } from "@wrnexus/db"; import type { Db, ExecResult } from "@wrnexus/db";
import { users } from "./schema.ts"; import { users } from "./schema.ts";
@@ -0,0 +1,49 @@
page ApiBlockDemo {
state nameFilter = "a"
state found = ""
state failed = ""
apis {
searchDirectory POST /api/directory {
request {
body {
name?: string
}
}
response {
return data.data.users
}
error {
return []
}
}
}
load server serverSearch {
const users = await api.searchDirectory({ name: "a" })
return { names: users.map((user) => user.name).join(", ") }
}
functions {
client async function search(): Promise<void> {
const users = await api.searchDirectory({ name: nameFilter })
found = users.map((user) => user.name).join(", ")
}
}
view {
<main>
<button @click="search()">Search</button>
<p class="found" data-text="found">{found}</p>
<p class="failed" data-text="failed">{failed}</p>
<Async source="serverSearch">
<Loading><p class="server-found">Loading…</p></Loading>
<Success data="serverSearch"><p class="server-found">{serverSearch.names}</p></Success>
<Error error="error"><p class="server-found">Failed: {error.message}</p></Error>
</Async>
<p class="bound" api="searchDirectory({ name: 'a' })">loading</p>
</main>
}
}
+20 -25
View File
@@ -18,37 +18,32 @@ page Hello {
canonical = "/hello" canonical = "/hello"
} }
// SSR data bindings run on the server before the HTML is sent. // API calls used by this page. `ssrUsers` is bound in the initial render
// The API itself lives in app/api/users/ssr.ts; this block only calls it // (its endpoint lives in app/api/users/ssr.ts); `csrUsers` is bound after
// and renders the response into HTML. // hydration in the browser (app/api/users/csr.ts). Both just call the same
ssr { // users endpoint shape, so the response handling looks the same.
functions { apis {
function userNames(users) { ssrUsers GET /api/users/ssr {
return users.map((user) => user.name).join(", ") response {
const visits = Number(cookies.get("hello_visits") ?? "0") + 1
cookies.set("hello_visits", String(visits), { sameSite: "Lax" })
session.set("lastHelloVisit", visits)
return `${userNames(data.users)} - visit ${visits}`
} }
} }
api ssrUsers GET /api/users/ssr { csrUsers GET /api/users/csr {
const visits = Number(cookies.get("hello_visits") ?? "0") + 1 response {
cookies.set("hello_visits", String(visits), { sameSite: "Lax" }) const label = localStorage.get("wrnexus.label") ?? "browser"
session.set("lastHelloVisit", visits) session.set("lastClientLabel", label)
return `${userNames(users)} - visit ${visits}` return `${userNames(data.users)} - ${label}`
}
} }
} }
// Client data bindings hydrate after the first paint. The browser only sees functions {
// an opaque data-wrnexus-csr id; WrNexus calls app/api/users/csr.ts on the server. shared function userNames(users) {
client { return users.map((user) => user.name).join(", ")
functions {
function userNames(users) {
return users.map((user) => user.name).join(", ")
}
}
api csrUsers GET /api/users/csr {
const label = localStorage.get("wrnexus.label") ?? "browser"
session.set("lastClientLabel", label)
return `${userNames(users)} - ${label}`
} }
} }
+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}`);
},
},
}, },
}); });
+3
View File
@@ -4,6 +4,7 @@
export interface Routes { export interface Routes {
"/": Record<string, never>; "/": Record<string, never>;
"/about": Record<string, never>; "/about": Record<string, never>;
"/api-block-demo": Record<string, never>;
"/async-data": Record<string, never>; "/async-data": Record<string, never>;
"/chat": Record<string, never>; "/chat": Record<string, never>;
"/client-only": Record<string, never>; "/client-only": Record<string, never>;
@@ -27,6 +28,7 @@ export interface Routes {
export interface RouteNames { export interface RouteNames {
"index": "/"; "index": "/";
"about": "/about"; "about": "/about";
"api.block.demo": "/api-block-demo";
"async.data": "/async-data"; "async.data": "/async-data";
"chat": "/chat"; "chat": "/chat";
"client.only": "/client-only"; "client.only": "/client-only";
@@ -119,6 +121,7 @@ export function route<N extends RouteName>(
const paths: Record<RouteName, RoutePath> = { const paths: Record<RouteName, RoutePath> = {
"index": "/", "index": "/",
"about": "/about", "about": "/about",
"api.block.demo": "/api-block-demo",
"async.data": "/async-data", "async.data": "/async-data",
"chat": "/chat", "chat": "/chat",
"client.only": "/client-only", "client.only": "/client-only",
@@ -0,0 +1,5 @@
import { v } from "@wrnexus/validation";
export const SearchDirectorySchema = v.object({
name: v.string().trim().optional(),
});
@@ -0,0 +1,7 @@
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
//
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
// checked; this file is compiled and checked normally by the project's own tsc.
export type __wrn_api_check_1fljm5i_searchDirectory = WRNexusGenerated.__wrn_expect_true<WRNexusGenerated.AssertAssignable<{ name?: string }, WRNexusGenerated.ApiInput<"/api/directory", "POST">>>;
export {};
+13 -2
View File
@@ -13,8 +13,8 @@ declare namespace WRNexusGenerated {
: never; : never;
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown; type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown; type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
type RouteName = "about" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "island.demo" | "language.tools" | "layout" | "login" | "modal" | "navigation" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "table" | "test" | "ui"; type RouteName = "about" | "api.block.demo" | "async.data" | "chat" | "client.only" | "dashboard" | "hello" | "index" | "island.demo" | "language.tools" | "layout" | "login" | "modal" | "navigation" | "partial.static" | "platform.showcase" | "reactive" | "server.actions" | "table" | "test" | "ui";
type ApiRoute = "/api/accounts" | "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/invite" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment"; type ApiRoute = "/api/accounts" | "/api/directory" | "/api/echo" | "/api/graphql-example" | "/api/hello" | "/api/invite" | "/api/login" | "/api/logout" | "/api/me" | "/api/typed-user" | "/api/users/csr" | "/api/users/ssr" | "/api/webhooks/payment";
type RealtimeRoute = "/realtime/chat" | "/realtime/hello"; type RealtimeRoute = "/realtime/chat" | "/realtime/hello";
type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY"; type EnvironmentKey = "APP_LABEL" | "DATABASE_URL" | "DEMO_SHARED" | "HOST" | "NODE_ENV" | "PORT" | "SESSION_SECRET" | "UAT_ONLY";
type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.layout" | "nav.navigation" | "nav.ui"; type TranslationKey = "api.greeting" | "home.intro" | "home.title" | "nav.about" | "nav.chat" | "nav.dashboard" | "nav.home" | "nav.layout" | "nav.navigation" | "nav.ui";
@@ -29,6 +29,7 @@ declare namespace WRNexusGenerated {
"/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> }; "/api/users/ssr": { GET: ApiContract<typeof import("../api/users/ssr.ts")["GET"]> };
"/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> }; "/api/graphql-example": { POST: ApiContract<typeof import("../api/graphql-example.ts")["POST"]> };
"/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> }; "/api/typed-user": { POST: ApiContract<typeof import("../api/typed-user.ts")["POST"]> };
"/api/directory": { POST: ApiContract<typeof import("../api/directory.ts")["POST"]> };
"/api/accounts": { GET: ApiContract<typeof import("../api/accounts.ts")["GET"]> }; "/api/accounts": { GET: ApiContract<typeof import("../api/accounts.ts")["GET"]> };
"/api/invite": { POST: ApiContract<typeof import("../api/invite.ts")["POST"]> }; "/api/invite": { POST: ApiContract<typeof import("../api/invite.ts")["POST"]> };
"/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> }; "/api/logout": { POST: ApiContract<typeof import("../api/logout.ts")["POST"]> };
@@ -57,4 +58,14 @@ declare namespace WRNexusGenerated {
"welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>; "welcome-email": QueuePayload<(typeof import("../queues/welcome-email.ts"))["default"]>;
} }
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"]; type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
type AssertAssignable<Actual, Expected> = unknown extends Expected
? true
: [Actual] extends [Expected]
? [Exclude<keyof Actual, keyof Expected>] extends [never]
? true
: false
: false;
type __wrn_expect_true<T extends true> = T;
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
} }
+3
View File
@@ -1,6 +1,9 @@
{ {
"name": "basic-app", "name": "basic-app",
"version": "0.8.0", "version": "0.8.0",
"wrnexus": {
"version": "0.9.0"
},
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+276
View File
@@ -0,0 +1,276 @@
# WrNexus
> WrNexus is an SSR-first, **Bun-native** full-stack web framework. UI is written in
> `.wrn` files (its own component language — NOT React/JSX/Vue). Routing is file-based.
> This document teaches an AI how to write correct WrNexus code. It is private and
> post-dates model training data, so rely on THIS document, not prior web-framework
> assumptions.
## Golden rules
- **Pages, components, and layouts are `.wrn` files.** Do NOT write `.tsx`/`.jsx`/React
for UI. Do NOT use `useState`, hooks, JSX, or a client bundler.
- **Routing is file-based** under `app/`. The filename is the route. No router config.
- **Interactivity** lives in `state` + `{expr}` + `@event` inside `.wrn`. Components render
on the server and hydrate automatically — you never write client-side JS islands.
- **Runtime is Bun only** (uses `Bun.serve`, `bun:sqlite`, `Bun.password`, …). Node is not supported.
- To add files, prefer the CLI: `wrnexus generate page <Name>` / `component <name>` / `api <path>` / `schema <name>`.
## Project layout
```
app/
pages/ *.wrn → routes: index.wrn = "/", about.wrn = "/about", blog/[slug].wrn = "/blog/:slug"
components/ *.wrn → reusable UI, mounted in a page/component via <div data-component="name" ...props>
layouts/ *.wrn → named layouts; a page opts in with layout = "name"
api/ *.ts → HTTP handlers: export const GET/POST/PUT/PATCH/DELETE = async (ctx) => Response
middleware/ *.ts → export default async (ctx, next) => next()
realtime/ *.ts → export default defineRoom({ ... }) from "@wrnexus/core" (ws://host/realtime/<name>)
schemas/ *.ts → validation schemas (the `v` builder), used by forms + parseBody
locales/ *.json → i18n messages per language
db/ schema.ts, queries/*.sql, migrations/*.sql
styles/ global.css → Tailwind (default) or plain CSS
wrnexus.config.ts → app config (AppConfig from "@wrnexus/styles")
public/ → static assets served at /
```
## `.wrn` page
```wrn
page Home {
layout = "public" // optional: a component in app/layouts/<name>.wrn ("none" to skip)
state count = 0 // optional: seeds client-reactive state (omit for pure SSR)
seo {
title = "Home"
description = "..."
canonical = "/"
}
view {
<h1>Hello</h1>
<p>Count is {count}, doubled is {count * 2}.</p>
<button @click="count++">Increment</button>
<div data-component="counter" start="5" label="Clicks"></div>
}
style {
h1 { color: var(--wrn-color-text); }
}
}
```
## `.wrn` component
```wrn
component Counter {
props { // props come from mount attributes; each is coerced to the
start = 0 // TYPE of its default (so start="5" arrives as the number 5)
label = "Count"
}
state count = start // state may reference props
view {
<button @click="count++">{label}: {count}</button>
}
}
```
Mount it from any page/component: `<div data-component="counter" start="0" label="Clicks"></div>`.
Components render on the server with their props, then hydrate — no per-component JS.
## The `view { }` block (plain HTML + a few directives)
- `{expr}` — interpolate a JS expression. Reactive if it references `state`: `{count}`, `{count * 2}`, `{user.name}`.
- `@event="expr"` — bind a DOM event; the expression runs in the reactive scope: `@click="count++"`, `@input="name = event.target.value"`.
- `<div data-component="name" prop="v">` — mount a component (attrs become string props, coerced).
- `<slot></slot>` / `<slot name="x"></slot>` — component/layout slots; fill with `<div data-slot="x">…</div>`.
- **Server loop (DB/list/table):** `{#each <list> as <item>[, <i>]} …rows… {:empty} …fallback… {/each}` — iterates SSR data on the server and renders markup per item. `{item.field}` interpolates (HTML-escaped, XSS-safe). `<list>` is a JS expression, usually an `ssr` data binding (see "Data-driven tables" below). This is how you render a database table in `.wrn`.
- **Server conditional:** `{#if <expr>} … {:else if <expr>} … {:else} … {/if}` — renders the first truthy branch on the server. `<expr>` can reference `ssr` data, or the `item`/`index` of an enclosing `{#each}`. Works at page level and inside loops (e.g. `{#if r.active}<span>●</span>{:else}<span>○</span>{/if}` per row). For client-side show/hide based on reactive `state`, use `data-show="expr"` instead.
- i18n: `{t:home.title}` in text, `t:placeholder="form.name"` on attributes — resolved per request from `app/locales/`.
- Theme: any element with `data-wrn-theme-toggle` toggles light/dark; `data-wrn-theme-set="dark"` sets it.
- Void/self-closing tags are fine: `<br />`, `<img src="..." />`.
- Only `{` and `}` are special (interpolation). Don't use a bare `}` in view text.
## Data-driven tables / lists (server-rendered `.wrn`)
Use an `ssr` data binding to fetch rows on the server, then `{#each}` to render them.
This renders on the **server** (SSR-first) and is HTML-escaped by default.
```wrn
page Admin {
layout = "dashboard"
// Fetch on the server. The api handler at /api/contacts returns { contacts: [...] };
// this block's `return contacts` exposes that array (via `$data`) as the binding `rows`.
ssr {
api rows GET /api/contacts { return contacts }
}
view {
<table>
<tbody>
{#each rows as r, i}
<tr>
<td>#{i}</td>
<td>{r.name}</td>
<td><a href="mailto:{r.email}">{r.email}</a></td>
</tr>
{:empty}
<tr><td colspan="3">No submissions yet.</td></tr>
{/each}
</tbody>
</table>
}
}
```
The matching API returns the array under a key the `ssr` block reads:
```ts
// app/api/contacts.ts → GET /api/contacts
import { getDb } from "@wrnexus/db";
export const GET = async () => {
const contacts = await getDb().all("SELECT id, name, email FROM contacts ORDER BY id DESC");
return Response.json({ contacts }); // ssr block does `return contacts`
};
```
**Prefer this `.wrn` + `{#each}` approach for DB-backed tables and lists.** (`.ts`/`.tsx`
pages returning an HTML string are also supported for fully-custom programmatic rendering,
but a `.wrn` page with `ssr` data + `{#each}` is the idiomatic, SSR-first way.)
## API routes (`app/api/*.ts`)
```ts
// app/api/users/list.ts → GET /api/users/list
import { getDb } from "@wrnexus/db";
export const GET = async (ctx) => {
return Response.json({ users: await ListUsers(getDb()) });
};
export const POST = async (ctx) => {
const body = await ctx.req.json();
return Response.json({ ok: true, body }, { status: 201 });
};
```
`ctx` (the `Context` from `@wrnexus/core`) has:
`req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
`@wrnexus/helpers` instead of constructing it from untrusted headers:
```ts
import { redirectToLogin } from "@wrnexus/helpers";
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
```
The package also exports `getOriginalRequestUrl`, `getOriginalRequestOrigin`,
`getOriginalRequestPath`, and `getOriginalRequestMethod`. Always pass `allowedHosts` when
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
## Middleware & realtime
```ts
// app/middleware/logger.ts
export default async function logger(ctx, next) {
console.log(ctx.req.method, ctx.url.pathname);
return next(); // return a Response WITHOUT calling next() to short-circuit
}
```
```ts
// app/realtime/chat.ts → ws://host/realtime/chat
import { defineRoom } from "@wrnexus/core";
export default defineRoom({
onConnect(client) { client.send({ type: "system", text: "connected" }); },
onMessage(client, msg) { client.room.broadcast({ type: "message", data: msg }); },
});
```
Client side: a page opts in with `data-room="chat"` (handled by the realtime runtime).
## Config (`wrnexus.config.ts`)
```ts
import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = {
seo: { title: "App", titleTemplate: "%s | App", description: "..." },
styles: { entry: "app/styles/global.css", process: async ({ entryPath, mode }) => /* Tailwind */ "" },
fonts: { sans: '"Inter", system-ui, sans-serif', google: [{ family: "Inter", weights: [400, 600] }] },
theme: { default: "dark", themes: { light: { "color-primary": "#2563eb" } } },
i18n: { default: "en", locales: ["en", "es"] },
db: { driver: "sqlite", url: "file:./dev.db" },
security: { cors: { enabled: true, origin: ["http://localhost:5173"] } },
// profiles: { production: { db: { driver: "postgres", url: process.env.DATABASE_URL } } },
};
export default config;
```
## Database (`@wrnexus/db`)
```ts
// app/db/schema.ts
import { v, table } from "@wrnexus/db";
export const users = table("users", {
id: v.id(),
name: v.string(),
email: v.string().unique(),
createdAt: v.timestamp(),
});
```
- Queries: write `app/db/queries/*.sql` with `-- name: ListUsers :many` blocks; `wrnexus db generate` emits typed functions.
- Access at runtime: `import { getDb } from "@wrnexus/db"; const rows = await ListUsers(getDb());`
- Migrations in `app/db/migrations/`; run `wrnexus db migrate` (dev auto-migrates sqlite).
## Validation (`@wrnexus/validation`)
```ts
// app/schemas/login.ts
import { v } from "@wrnexus/validation";
export default v.object({
email: v.string().email(),
password: v.string().min(8),
});
```
In an API route: `import s from "../schemas/login"; import { parseBody } from "@wrnexus/validation"; const r = await parseBody(s, ctx.req);` → `r.ok ? r.value : r.response`.
In a form: `<form data-schema="login" action="/api/login" method="post">` + `<span data-error="email"></span>` (client + server validation connected automatically).
## AI / LLM (`@wrnexus/ai`)
```ts
// app/api/ai.ts
import { createAI } from "@wrnexus/ai";
const ai = createAI(); // reads ANTHROPIC_API_KEY; default model claude-opus-4-8
export const POST = async (ctx) => {
const { prompt } = await ctx.req.json();
return ai.streamResponse(prompt); // or: return Response.json({ text: await ai.generate(prompt) })
};
```
## CLI
```
wrnexus dev . # dev server + HMR
wrnexus build . # production build → dist/server.js
bun dist/server.js # run the production server (or npm start)
wrnexus create <name> # scaffold a new app
wrnexus update --latest # deps + syntax/config migrations + verification
wrnexus generate page <Name> # scaffold a page (aliases: g p)
wrnexus generate component <name> | api <path> | schema <name>
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
wrnexus eject <component> # copy a WrNexus UI component's .wrn into app/components to customize
```
## When asked to "create a page/component/feature"
1. Create the `.wrn` file under `app/pages/` (or `app/components/`) with a `page`/`component` block — or run `wrnexus generate page <Name>`.
2. Put markup in `view { }`, interactive bits in `state` + `{expr}` + `@event`, reusable UI as components mounted via `data-component`.
3. For data, add an `app/api/*.ts` route and `getDb()`; for forms, add an `app/schemas/*.ts` and `data-schema`.
4. Style with Tailwind utility classes in the view, or theme tokens (`var(--wrn-*)`), or `style { }`.
5. Never emit React/JSX, a manual router, or client-side island JS — the framework handles hydration.
-2
View File
@@ -13,8 +13,6 @@ import type { AppConfig } from "@wrnexus/styles";
* framework-level headers and optional CORS. * framework-level headers and optional CORS.
*/ */
const config: AppConfig = { const config: AppConfig = {
frameworkBehaviour: 1,
compatibilityDate: "2026-08-02",
head: [ head: [
// --- Use a CSS framework via CDN (uncomment one) --- // --- Use a CSS framework via CDN (uncomment one) ---
// Bootstrap: // Bootstrap:
+5
View File
@@ -0,0 +1,5 @@
dist/
.wrnexus/
*.sqlite
*.sqlite-shm
*.sqlite-wal
+13
View File
@@ -0,0 +1,13 @@
# Northstar CRM example
A complete WrNexus example covering public pages, SQLite migrations and seed data, SQL-backed signup/login/session authentication, database-backed roles and permissions, protected pages, and permission-checked CRM APIs.
```bash
bun run db:migrate
bun run db:seed
bun run dev
```
Open `/signup`, create an account, and the signup hook assigns the `sales-rep` role. The user is automatically signed in and redirected to `/dashboard`. Auth tables come from the `@wrnexus/auth` plugin migrations; CRM and authorization tables come from `app/db/migrations/001_crm.sql`.
The seed records use the placeholder owner `demo-owner` to demonstrate repeatable data. To attach them to a registered user, update their `owner_id` to that user's ID.
+35
View File
@@ -0,0 +1,35 @@
import { can } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import type { Context } from "@wrnexus/core";
import { ensureCrmWorkspace } from "../lib/workspace.ts";
const userId = (ctx: Context) => String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
export async function GET(ctx: Context): Promise<Response> {
if (!(await can(ctx, "contact:read")))
return Response.json({ error: "Forbidden" }, { status: 403 });
await ensureCrmWorkspace(userId(ctx));
const rows = await getDb().all(
"SELECT id,name,email,company,phone,status,created_at FROM crm_contacts WHERE owner_id = ? ORDER BY name",
[userId(ctx)],
);
return Response.json({ contacts: rows });
}
export async function POST(ctx: Context): Promise<Response> {
if (!(await can(ctx, "contact:write")))
return Response.json({ error: "Forbidden" }, { status: 403 });
const body = (await ctx.req.json()) as Record<string, unknown>;
const name = String(body.name ?? "").trim();
const email = String(body.email ?? "")
.trim()
.toLowerCase();
if (!name || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return Response.json({ error: "A name and valid email are required" }, { status: 400 });
}
const result = await getDb().exec(
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[userId(ctx), name, email, String(body.company ?? ""), String(body.phone ?? ""), "lead"],
);
return Response.json({ id: Number(result.lastInsertId), name, email }, { status: 201 });
}
+33
View File
@@ -0,0 +1,33 @@
import { can } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import type { Context } from "@wrnexus/core";
import { ensureCrmWorkspace } from "../lib/workspace.ts";
export async function GET(ctx: Context): Promise<Response> {
if (!(await can(ctx, "crm:dashboard")))
return Response.json({ error: "Forbidden" }, { status: 403 });
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
await ensureCrmWorkspace(owner);
const metrics = await getDb().one<{
pipeline_value_cents: number;
open_opportunities: number;
contacts: number;
}>(
`SELECT
COALESCE((SELECT SUM(value_cents) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')), 0) pipeline_value_cents,
(SELECT COUNT(*) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')) open_opportunities,
(SELECT COUNT(*) FROM crm_contacts WHERE owner_id = ?) contacts`,
[owner, owner, owner],
);
return Response.json({
metrics: {
pipelineValue: new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(Number(metrics?.pipeline_value_cents ?? 0) / 100),
openOpportunities: Number(metrics?.open_opportunities ?? 0),
contacts: Number(metrics?.contacts ?? 0),
},
});
}
+15
View File
@@ -0,0 +1,15 @@
import { can } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import type { Context } from "@wrnexus/core";
import { ensureCrmWorkspace } from "../lib/workspace.ts";
export async function GET(ctx: Context): Promise<Response> {
if (!(await can(ctx, "deal:read"))) return Response.json({ error: "Forbidden" }, { status: 403 });
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
await ensureCrmWorkspace(owner);
const deals = await getDb().all(
"SELECT d.id,d.title,d.value_cents,d.stage,d.close_date,c.name contact_name FROM crm_deals d LEFT JOIN crm_contacts c ON c.id=d.contact_id WHERE d.owner_id=? ORDER BY d.updated_at DESC",
[owner],
);
return Response.json({ deals });
}
+19
View File
@@ -0,0 +1,19 @@
import { defineAuthz } from "@wrnexus/authz";
export default defineAuthz({
permissions: {
"crm:dashboard": { title: "View dashboard" },
"contact:read": { title: "View contacts" },
"contact:write": { title: "Create and edit contacts" },
"contact:delete": { title: "Delete contacts", risk: "high" },
"deal:read": { title: "View deals" },
"deal:write": { title: "Create and edit deals" },
"admin:access": { title: "Manage CRM access", risk: "high" },
},
roles: {
viewer: ["crm:dashboard", "contact:read", "deal:read"],
"sales-rep": ["role:viewer", "contact:write", "deal:write"],
manager: ["role:sales-rep", "contact:delete"],
admin: ["role:manager", "admin:access"],
},
});
@@ -0,0 +1,65 @@
-- +up
CREATE TABLE IF NOT EXISTS crm_contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL,
company TEXT NOT NULL DEFAULT '',
phone TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'lead' CHECK (status IN ('lead', 'customer', 'inactive')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS crm_contacts_owner_idx ON crm_contacts(owner_id);
CREATE UNIQUE INDEX IF NOT EXISTS crm_contacts_owner_email_uq ON crm_contacts(owner_id, email);
CREATE TABLE IF NOT EXISTS crm_deals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_id TEXT NOT NULL,
contact_id INTEGER REFERENCES crm_contacts(id) ON DELETE SET NULL,
title TEXT NOT NULL,
value_cents INTEGER NOT NULL DEFAULT 0 CHECK (value_cents >= 0),
stage TEXT NOT NULL DEFAULT 'qualified' CHECK (stage IN ('qualified', 'proposal', 'won', 'lost')),
close_date TEXT,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS crm_deals_owner_idx ON crm_deals(owner_id);
CREATE INDEX IF NOT EXISTS crm_deals_contact_idx ON crm_deals(contact_id);
CREATE TABLE IF NOT EXISTS crm_activities (
id INTEGER PRIMARY KEY AUTOINCREMENT,
actor_id TEXT NOT NULL,
entity_type TEXT NOT NULL CHECK (entity_type IN ('contact', 'deal', 'account')),
entity_id TEXT NOT NULL,
action TEXT NOT NULL,
details_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS crm_activities_entity_idx ON crm_activities(entity_type, entity_id);
-- Database-backed authorization assignments.
CREATE TABLE IF NOT EXISTS _wrn_authz_assignment (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id VARCHAR(255) NOT NULL,
scope VARCHAR(255) NOT NULL DEFAULT '',
role VARCHAR(255) NOT NULL,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT _wrn_authz_assignment_unique UNIQUE (subject_id, scope, role)
);
CREATE TABLE IF NOT EXISTS _wrn_authz_grant (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subject_id VARCHAR(255) NOT NULL,
scope VARCHAR(255) NOT NULL DEFAULT '',
permission VARCHAR(255) NOT NULL,
effect VARCHAR(16) NOT NULL CHECK (effect IN ('allow', 'deny')),
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT _wrn_authz_grant_unique UNIQUE (subject_id, scope, permission)
);
-- +down
DROP TABLE IF EXISTS _wrn_authz_grant;
DROP TABLE IF EXISTS _wrn_authz_assignment;
DROP TABLE IF EXISTS crm_activities;
DROP TABLE IF EXISTS crm_deals;
DROP TABLE IF EXISTS crm_contacts;
+23
View File
@@ -0,0 +1,23 @@
import type { Db } from "@wrnexus/db";
export default async function seed(db: Db): Promise<void> {
const owner = "demo-owner";
await db.exec("DELETE FROM crm_activities");
await db.exec("DELETE FROM crm_deals");
await db.exec("DELETE FROM crm_contacts");
await db.exec(
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[owner, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
);
await db.exec(
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[owner, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
);
const contact = await db.one<{ id: number }>("SELECT id FROM crm_contacts WHERE email = ?", [
"ada@example.test",
]);
await db.exec(
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
[owner, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
);
}
+32
View File
@@ -0,0 +1,32 @@
import { createAuthEngine, SqlAuthStore, type AuthStore } from "@wrnexus/auth";
import { getDb } from "@wrnexus/db";
import { dbPermissionStore } from "@wrnexus/authz/db";
import { ensureCrmWorkspace } from "./workspace.ts";
// Config is imported before the runtime opens its configured database. This
// forwarding store resolves getDb() only when an auth operation actually runs.
const store = new Proxy({} as AuthStore, {
get(_target, property) {
const value = Reflect.get(new SqlAuthStore(getDb()), property);
return typeof value === "function" ? value.bind(new SqlAuthStore(getDb())) : value;
},
});
export const auth = createAuthEngine({
store,
secret: process.env.AUTH_SECRET ?? "northstar-crm-development-secret-change-me",
issuer: "Northstar CRM",
onSignedIn(ctx, returnTo) {
const destination =
returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/dashboard";
return Response.redirect(new URL(destination, ctx.url), 303);
},
onSignedOut(ctx) {
return Response.redirect(new URL("/", ctx.url), 303);
},
async onSuccessfulSignUp(_ctx, user) {
await dbPermissionStore(getDb()).assignRole(user.id, "sales-rep");
await ensureCrmWorkspace(user.id);
return { autoSignIn: true, redirectTo: "/dashboard" };
},
});
+29
View File
@@ -0,0 +1,29 @@
import { getDb } from "@wrnexus/db";
/** Give every real CRM user a useful workspace on first access. */
export async function ensureCrmWorkspace(ownerId: string): Promise<void> {
if (!ownerId) return;
const db = getDb();
await db.exec(
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[ownerId, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
);
await db.exec(
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
[ownerId, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
);
const existingDeal = await db.one<{ count: number }>(
"SELECT COUNT(*) count FROM crm_deals WHERE owner_id = ?",
[ownerId],
);
if (Number(existingDeal?.count ?? 0) === 0) {
const contact = await db.one<{ id: number }>(
"SELECT id FROM crm_contacts WHERE owner_id = ? AND email = ?",
[ownerId, "ada@example.test"],
);
await db.exec(
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
[ownerId, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
);
}
}
+11
View File
@@ -0,0 +1,11 @@
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
import { getDb } from "@wrnexus/db";
import { dbPermissionStore } from "@wrnexus/authz/db";
import type { Middleware } from "@wrnexus/core";
// Production imports middleware before it opens configured databases. Resolve
// the SQL-backed store on the first request, after runtime initialization.
const middleware: Middleware = (ctx, next) =>
authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) })(ctx, next);
export default middleware;
@@ -0,0 +1,18 @@
import { requireAuth } from "@wrnexus/auth";
import { can } from "@wrnexus/authz";
import type { Middleware } from "@wrnexus/core";
const guard = requireAuth({ loginPath: "/login" });
const protectedPrefixes = ["/dashboard", "/contacts", "/deals", "/admin"];
const middleware: Middleware = (ctx, next) => {
if (!protectedPrefixes.some((prefix) => ctx.url.pathname.startsWith(prefix))) return next();
return guard(ctx, async () => {
if (ctx.url.pathname.startsWith("/admin") && !(await can(ctx, "admin:access"))) {
return Response.redirect(new URL("/forbidden", ctx.url), 303);
}
return next();
});
};
export default middleware;
+5
View File
@@ -0,0 +1,5 @@
page Admin {
state user = ctx.user
seo { title = "Administration" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin" aria-current="page">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Access administration</h1><p>Manage roles and protect high-risk operations.</p></div></header><section class="crm-panel"><span class="crm-eyebrow">Role-based access</span><h2>Signed in as {user.displayName || user.username}</h2><p class="crm-hero-copy">This route requires the <strong>admin:access</strong> permission. Assign manager and administrator roles through the database-backed authorization store.</p></section></section></main> }
}
+6
View File
@@ -0,0 +1,6 @@
page Contacts {
apis { listContacts GET /api/contacts { response { return data.contacts } } }
load server contacts { return await api.listContacts() }
seo { title = "Contacts" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts" aria-current="page">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Contacts</h1><p>Every relationship, scoped securely to its owner.</p></div><a class="crm-button" href="mailto:sales@example.test">New contact</a></header><section class="crm-panel"><span class="crm-eyebrow">Customer directory</span>{#if contacts.length}<ul class="crm-list">{#each contacts as contact}<li><strong>{contact.name}</strong><span>{contact.company || contact.email}</span><span class="crm-pill">{contact.status}</span></li>{/each}</ul>{/if}{#if !contacts.length}<div class="crm-empty"><h2>No contacts yet</h2><p>Your first customer relationship will appear here.</p></div>{/if}</section></section></main> }
}
+8
View File
@@ -0,0 +1,8 @@
page Dashboard {
state user = ctx.user
state dashboardMetrics = ctx.metrics
apis { dashboard GET /api/dashboard { response { return data.metrics } } }
load server metrics { return await api.dashboard() }
seo { title = "Dashboard" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard" aria-current="page">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav><form class="crm-sidebar-footer" method="post" action="/api/auth/logout" data-schema="auth-empty"><button class="crm-button crm-button--ghost" type="submit">Log out</button></form></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Good to see you, {user.displayName || user.username}</h1><p>Live figures from your private SQLite workspace.</p></div><a class="crm-button" href="/contacts">View contacts</a></header><div class="crm-stat-grid"><article class="crm-stat"><span>Pipeline value</span><strong>{dashboardMetrics.pipelineValue}</strong></article><article class="crm-stat"><span>Open opportunities</span><strong>{dashboardMetrics.openOpportunities}</strong></article><article class="crm-stat"><span>Total contacts</span><strong>{dashboardMetrics.contacts}</strong></article></div><section class="crm-panel"><span class="crm-eyebrow">Server rendered</span><h2>One secure data path</h2><p class="crm-hero-copy">This page was rendered from authenticated API results on the server. No browser prefetch or client data request is needed to show these metrics.</p></section></section></main> }
}
+6
View File
@@ -0,0 +1,6 @@
page Deals {
apis { listDeals GET /api/deals { response { return data.deals } } }
load server deals { return await api.listDeals() }
seo { title = "Deals" }
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals" aria-current="page">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Sales pipeline</h1><p>Focus on the opportunities most likely to move.</p></div><a class="crm-button" href="/contacts">New deal</a></header><section class="crm-panel"><span class="crm-eyebrow">Open opportunities</span>{#if deals.length}<ul class="crm-list">{#each deals as deal}<li><strong>{deal.title}</strong><span>{deal.contact_name || "Unassigned contact"}</span><span class="crm-pill">{deal.stage}</span></li>{/each}</ul>{/if}{#if !deals.length}<div class="crm-empty"><h2>No open deals</h2><p>Create a contact and turn the relationship into an opportunity.</p></div>{/if}</section></section></main> }
}
+6
View File
@@ -0,0 +1,6 @@
page Forbidden {
seo { title = "Access denied" }
view {
<main class="crm-status-page"><section class="crm-status-card"><span class="crm-status-icon" aria-hidden="true">!</span><span class="crm-eyebrow">Permission required</span><h1>You do not have access to administration.</h1><p>Your account is working correctly, but an administrator role is required for this page.</p><div class="crm-actions"><a class="crm-button" href="/dashboard">Back to dashboard</a><a class="crm-button crm-button--ghost" href="/contacts">View contacts</a></div></section></main>
}
}
+31
View File
@@ -0,0 +1,31 @@
page Home {
seo { title = "CRM that keeps sales moving" }
view {
<main
class="crm-public"
>
<nav
class="crm-nav"
>
<a
class="crm-brand"
href="/"
>
<span
class="crm-brand-mark"
>
N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/pricing">Pricing</a><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div>
</nav>
<section
class="crm-hero"
>
<div>
<span
class="crm-eyebrow"
>
Customer intelligence, simplified</span><h1>Know every customer. Close every opportunity.</h1><p class="crm-hero-copy">Bring contacts, conversations, and pipeline into one calm workspace built for teams that value clarity.</p><div class="crm-actions"><a class="crm-button" href="/signup">Create your workspace</a><a class="crm-button crm-button--ghost" href="/login">View the CRM</a></div></div><div class="crm-preview"><div class="crm-preview-bar"><span></span><span></span><span></span></div><div class="crm-preview-grid"><article class="crm-preview-card"><small>Pipeline value</small><strong>$125k</strong><small>+18% this month</small></article><article class="crm-preview-card"><small>Active contacts</small><strong>248</strong><small>32 need follow-up</small></article><article class="crm-preview-card"><small>Win rate</small><strong>42%</strong><small>Across qualified deals</small></article><article class="crm-preview-card"><small>Next action</small><strong>8 today</strong><small>Stay ahead of every promise</small></article></div>
</div>
</section>
</main>
}
}
+10
View File
@@ -0,0 +1,10 @@
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
page Login {
seo { title = "Log in" }
view {
<AuthSplitLayout brand="Northstar CRM" eyebrow="Customer intelligence" title="Turn every conversation into momentum." description="Sign in to a focused workspace for contacts, pipeline, and activity." features='[{"label":"One customer timeline","description":"Every relationship and opportunity in context."},{"label":"Secure by default","description":"SQL sessions and role-based authorization."},{"label":"Built for focus","description":"A calm workspace without sales-tool clutter."}]'>
<div data-slot="form"><SignIn action="/api/auth/login" returnTo="/dashboard" signUpHref="/signup" forgotHref="/login" showPasskey="false" class="border-0 shadow-none" /></div>
</AuthSplitLayout>
}
}
+4
View File
@@ -0,0 +1,4 @@
page Pricing {
seo { title = "Pricing" }
view { <main class="crm-public"><nav class="crm-nav"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div></nav><section class="crm-hero"><div><span class="crm-eyebrow">Simple pricing</span><h1>Start free. Scale when your team does.</h1><p class="crm-hero-copy">Everything needed to evaluate a secure, full-stack CRM locally. Move to Team when you are ready to collaborate.</p></div><div class="crm-preview"><article class="crm-preview-card"><small>Starter</small><strong>$0</strong><p>SQLite workspace, CRM flows, authentication and permissions.</p><a class="crm-button" href="/signup">Start building</a></article><article class="crm-preview-card"><small>Team</small><strong>$29</strong><p>Per user/month with shared pipeline and administration.</p></article></div></section></main> }
}
+10
View File
@@ -0,0 +1,10 @@
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
page Signup {
seo { title = "Create account" }
view {
<AuthSplitLayout brand="Northstar CRM" eyebrow="Start in minutes" title="Build relationships, not spreadsheets." description="Create your secure sales workspace and begin with a complete customer view." features='[{"label":"Free local workspace","description":"Explore the complete CRM flow with SQLite."},{"label":"Flexible permissions","description":"Viewer, sales, manager, and admin roles."},{"label":"Your data stays yours","description":"Portable SQL data and explicit migrations."}]'>
<div data-slot="form"><SignUp redirect="/dashboard" signInHref="/login" showPhone="false" showUsername="false" requireConsent="false" class="border-0 shadow-none" /></div>
</AuthSplitLayout>
}
}
+116
View File
@@ -0,0 +1,116 @@
// AUTO-GENERATED by `wrnexus dev` - do not edit.
// Typed routes support required, optional, and catch-all parameters.
export interface Routes {
"/": Record<string, never>;
"/admin": Record<string, never>;
"/contacts": Record<string, never>;
"/dashboard": Record<string, never>;
"/deals": Record<string, never>;
"/forbidden": Record<string, never>;
"/login": Record<string, never>;
"/pricing": Record<string, never>;
"/signup": Record<string, never>;
}
export interface RouteNames {
"index": "/";
"admin": "/admin";
"contacts": "/contacts";
"dashboard": "/dashboard";
"deals": "/deals";
"forbidden": "/forbidden";
"login": "/login";
"pricing": "/pricing";
"signup": "/signup";
}
export interface RouteQueries {
[path: string]: Record<string, string | number | boolean | null | undefined>;
}
export type RoutePath = keyof Routes;
export type RouteName = keyof RouteNames;
export type RouteQuery<P extends RoutePath> = P extends keyof RouteQueries
? RouteQueries[P]
: Record<string, string | number | boolean | null | undefined>;
type RouteValue = string | readonly string[] | undefined;
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
if (value === undefined) return "";
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
return values.map((part) => encodeURIComponent(part)).join("/");
}
function buildHref(path: string, params: Record<string, RouteValue> = {}): string {
const output: string[] = [];
for (const segment of path.split("/").filter(Boolean)) {
let name: string | undefined;
let optional = false;
let catchAll = false;
if (segment.startsWith("[[") && segment.endsWith("]]")) {
optional = true;
name = segment.slice(2, -2);
} else if (segment.startsWith("[") && segment.endsWith("]")) {
name = segment.slice(1, -1);
if (name.endsWith("?")) {
optional = true;
name = name.slice(0, -1);
}
}
if (!name) {
output.push(segment);
continue;
}
if (name.startsWith("...")) {
catchAll = true;
name = name.slice(3);
}
const value = params[name];
if (value === undefined && optional) continue;
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
output.push(encodeRouteValue(value, catchAll));
}
return "/" + output.filter(Boolean).join("/");
}
export function href<P extends RoutePath>(
path: P,
...args: keyof Routes[P] extends never
? []
: Record<string, never> extends Routes[P]
? [params?: Routes[P]]
: [params: Routes[P]]
): string {
const params = (args[0] ?? {}) as Record<string, RouteValue>;
return buildHref(String(path), params);
}
export function route<N extends RouteName>(
name: N,
...args: keyof Routes[RouteNames[N]] extends never
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
: Record<string, never> extends Routes[RouteNames[N]]
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
: [params: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
): string {
const paths: Record<RouteName, RoutePath> = {
"index": "/",
"admin": "/admin",
"contacts": "/contacts",
"dashboard": "/dashboard",
"deals": "/deals",
"forbidden": "/forbidden",
"login": "/login",
"pricing": "/pricing",
"signup": "/signup"
} as Record<RouteName, RoutePath>;
const output = buildHref(paths[name], (args[0] ?? {}) as Record<string, RouteValue>);
const query = args[1];
if (!query) return output;
const search = new URLSearchParams();
for (const [key, value] of Object.entries(query))
if (value !== undefined && value !== null) search.set(key, String(value));
const text = search.toString();
return text ? `${output}?${text}` : output;
}
+375
View File
@@ -0,0 +1,375 @@
@import "tailwindcss";
@plugin "@iconify/tailwind4";
@source "../**/*.wrn";
@source "../../../packages/auth/components/*.wrn";
@source "../../../packages/ui/components/*.wrn";
:root {
color-scheme: light;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
min-width: 320px;
background: var(--wrn-color-bg);
}
body {
color: var(--wrn-color-text);
background: var(--wrn-color-bg);
font-family: Inter, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
text-decoration: none;
}
.crm-public {
min-height: 100dvh;
overflow: hidden;
background:
radial-gradient(
circle at 80% 10%,
color-mix(in srgb, var(--wrn-color-primary) 15%, transparent),
transparent 28%
),
var(--wrn-color-bg);
}
.crm-nav {
width: min(1180px, calc(100% - 2rem));
margin: auto;
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0;
}
.crm-brand {
display: inline-flex;
align-items: center;
gap: 0.7rem;
font-weight: 800;
letter-spacing: -0.025em;
}
.crm-brand-mark {
display: grid;
width: 2.25rem;
height: 2.25rem;
place-items: center;
border-radius: 0.75rem;
color: white;
background: linear-gradient(135deg, var(--wrn-color-primary), #7c3aed);
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 28%, transparent);
}
.crm-nav-links {
display: flex;
align-items: center;
gap: 1.5rem;
color: var(--wrn-color-muted);
font-size: 0.9rem;
font-weight: 600;
}
.crm-button {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
justify-content: center;
padding: 0 1.1rem;
border-radius: 0.75rem;
color: white;
background: var(--wrn-color-primary);
font-weight: 700;
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 22%, transparent);
}
.crm-button--ghost {
color: var(--wrn-color-text);
background: var(--wrn-color-surface);
border: 1px solid var(--wrn-color-border);
box-shadow: none;
}
.crm-hero {
width: min(1180px, calc(100% - 2rem));
margin: auto;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
align-items: center;
gap: 4rem;
padding: clamp(4rem, 9vw, 8rem) 0;
}
.crm-eyebrow {
color: var(--wrn-color-primary);
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.13em;
text-transform: uppercase;
}
.crm-hero h1 {
max-width: 12ch;
margin: 1rem 0;
font-size: clamp(3rem, 7vw, 5.5rem);
line-height: 0.98;
letter-spacing: -0.065em;
}
.crm-hero-copy {
max-width: 38rem;
color: var(--wrn-color-muted);
font-size: 1.1rem;
line-height: 1.75;
}
.crm-actions {
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
margin-top: 2rem;
}
.crm-preview {
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: 1.5rem;
background: color-mix(in srgb, var(--wrn-color-surface) 88%, transparent);
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.15);
transform: rotate(1.5deg);
}
.crm-preview-bar {
display: flex;
gap: 0.4rem;
padding: 0.4rem 0.25rem 1rem;
}
.crm-preview-bar span {
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: var(--wrn-color-border);
}
.crm-preview-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.8rem;
}
.crm-preview-card {
min-height: 8rem;
padding: 1rem;
border-radius: 1rem;
background: var(--wrn-color-surface-2);
}
.crm-preview-card strong {
display: block;
margin-top: 0.7rem;
font-size: 1.6rem;
}
.crm-preview-card small {
color: var(--wrn-color-muted);
}
.crm-shell {
min-height: 100dvh;
display: grid;
grid-template-columns: 16rem 1fr;
background: var(--wrn-color-bg);
}
.crm-sidebar {
position: sticky;
top: 0;
height: 100dvh;
display: flex;
flex-direction: column;
padding: 1.35rem;
border-right: 1px solid var(--wrn-color-border);
background: var(--wrn-color-surface);
}
.crm-menu {
display: grid;
gap: 0.35rem;
margin-top: 2rem;
}
.crm-menu a {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0.85rem;
border-radius: 0.75rem;
color: var(--wrn-color-muted);
font-size: 0.9rem;
font-weight: 650;
}
.crm-menu a:hover,
.crm-menu a[aria-current="page"] {
color: var(--wrn-color-primary);
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
}
.crm-sidebar-footer {
margin-top: auto;
}
.crm-content {
min-width: 0;
padding: clamp(1.25rem, 4vw, 3rem);
}
.crm-topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 2rem;
}
.crm-topbar h1 {
margin: 0;
font-size: clamp(1.75rem, 4vw, 2.4rem);
letter-spacing: -0.04em;
}
.crm-topbar p {
margin: 0.4rem 0 0;
color: var(--wrn-color-muted);
}
.crm-panel {
padding: 1.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: 1rem;
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-1);
}
.crm-stat-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.crm-stat {
padding: 1.25rem;
border: 1px solid var(--wrn-color-border);
border-radius: 1rem;
background: var(--wrn-color-surface);
}
.crm-stat span {
color: var(--wrn-color-muted);
font-size: 0.8rem;
font-weight: 650;
}
.crm-stat strong {
display: block;
margin-top: 0.6rem;
font-size: 2rem;
letter-spacing: -0.04em;
}
.crm-list {
display: grid;
gap: 0.65rem;
padding: 0;
margin: 1rem 0 0;
list-style: none;
}
.crm-list li {
display: grid;
grid-template-columns: minmax(10rem, 1fr) minmax(8rem, 0.7fr) auto;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--wrn-color-border);
border-radius: 0.8rem;
background: var(--wrn-color-surface-2);
}
.crm-pill {
justify-self: end;
padding: 0.3rem 0.65rem;
border-radius: 999px;
color: var(--wrn-color-primary);
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
font-size: 0.72rem;
font-weight: 750;
text-transform: capitalize;
}
.crm-empty {
padding: 3rem 1rem;
text-align: center;
color: var(--wrn-color-muted);
}
.crm-empty h2 {
margin-bottom: 0.35rem;
color: var(--wrn-color-text);
}
.crm-status-page {
min-height: 100dvh;
display: grid;
place-items: center;
padding: 1.5rem;
background: radial-gradient(
circle at 50% 0%,
color-mix(in srgb, var(--wrn-color-primary) 12%, transparent),
transparent 45%
);
}
.crm-status-card {
width: min(34rem, 100%);
padding: clamp(1.5rem, 5vw, 3rem);
border: 1px solid var(--wrn-color-border);
border-radius: 1.25rem;
background: var(--wrn-color-surface);
box-shadow: var(--wrn-shadow-2);
}
.crm-status-card h1 {
margin: 0.9rem 0 0.7rem;
font-size: clamp(1.8rem, 5vw, 2.5rem);
line-height: 1.08;
letter-spacing: -0.045em;
}
.crm-status-card > p {
color: var(--wrn-color-muted);
line-height: 1.65;
}
.crm-status-icon {
display: grid;
width: 3rem;
height: 3rem;
margin-bottom: 1.25rem;
place-items: center;
border-radius: 1rem;
color: #b45309;
background: #fef3c7;
font-size: 1.4rem;
font-weight: 800;
}
@media (max-width: 820px) {
.crm-nav-links > a:not(.crm-button) {
display: none;
}
.crm-hero {
grid-template-columns: 1fr;
padding-top: 3rem;
}
.crm-preview {
transform: none;
}
.crm-shell {
grid-template-columns: 1fr;
}
.crm-sidebar {
position: static;
width: 100%;
height: auto;
}
.crm-menu {
grid-template-columns: repeat(4, 1fr);
overflow: auto;
}
.crm-sidebar-footer {
margin-top: 1rem;
}
.crm-stat-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.crm-hero h1 {
font-size: 3rem;
}
.crm-menu {
grid-template-columns: repeat(2, 1fr);
}
.crm-list li {
grid-template-columns: 1fr;
}
.crm-pill {
justify-self: start;
}
}
@@ -0,0 +1,7 @@
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
//
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
// checked; this file is compiled and checked normally by the project's own tsc.
export {};
+47
View File
@@ -0,0 +1,47 @@
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
declare namespace WRNexusGenerated {
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
? { input: I; output: O }
: T extends (...args: infer A) => infer R
? { input: A extends [any, infer I, ...any[]] ? I : unknown; output: Awaited<R> }
: { input: unknown; output: unknown };
type MiddlewareContext<T> = T extends (ctx: infer C, ...args: any[]) => any ? C : never;
type QueryContract<T> = T extends (db: any, args: infer A, ...rest: any[]) => infer R
? { args: A; result: Awaited<R> }
: T extends (db: any, ...rest: any[]) => infer R
? { args: Record<string, never>; result: Awaited<R> }
: never;
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
type RouteName = "admin" | "contacts" | "dashboard" | "deals" | "forbidden" | "index" | "login" | "pricing" | "signup";
type ApiRoute = "/api/contacts" | "/api/dashboard" | "/api/deals";
type RealtimeRoute = never;
type EnvironmentKey = never;
type TranslationKey = never;
type QueueName = never;
type CacheKey = never;
type Components = Record<string, never>;
interface ApiContracts {
"/api/dashboard": { GET: ApiContract<typeof import("../api/dashboard.ts")["GET"]> };
"/api/contacts": { GET: ApiContract<typeof import("../api/contacts.ts")["GET"]>; POST: ApiContract<typeof import("../api/contacts.ts")["POST"]> };
"/api/deals": { GET: ApiContract<typeof import("../api/deals.ts")["GET"]> };
}
interface MiddlewareContexts {
"authz": MiddlewareContext<(typeof import("../middleware/authz.ts"))["default"]>;
"protected": MiddlewareContext<(typeof import("../middleware/protected.ts"))["default"]>;
}
type DatabaseQueries = Record<string, never>;
type RealtimeMessages = Record<string, never>;
type QueuePayloads = Record<string, never>;
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
type AssertAssignable<Actual, Expected> = unknown extends Expected
? true
: [Actual] extends [Expected]
? [Exclude<keyof Actual, keyof Expected>] extends [never]
? true
: false
: false;
type __wrn_expect_true<T extends true> = T;
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
}
@@ -0,0 +1 @@
// AUTO-GENERATED plugin type aggregation - do not edit.
+30
View File
@@ -0,0 +1,30 @@
{
"name": "wrnexus-crm-example",
"version": "0.8.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run ../../packages/cli/src/index.ts dev .",
"build": "bun run ../../packages/cli/src/index.ts build .",
"db:migrate": "bun run ../../packages/cli/src/index.ts db migrate .",
"db:seed": "bun run ../../packages/cli/src/index.ts db seed .",
"test": "bun test test",
"typecheck": "tsc --noEmit -p tsconfig.json",
"check": "bun run typecheck && bun run test && bun run build"
},
"dependencies": {
"@wrnexus/auth": "workspace:*",
"@wrnexus/authz": "workspace:*",
"@wrnexus/core": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/styles": "workspace:*"
},
"devDependencies": {
"@iconify-json/lucide": "^1.2.123",
"@iconify/tailwind4": "^1.2.3",
"@tailwindcss/cli": "^4.3.3",
"@types/bun": "^1.3.14",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3"
}
}
+82
View File
@@ -0,0 +1,82 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { createDb, loadMigrations, migrate } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";
import seed from "../app/db/seed.ts";
const root = join(import.meta.dir, "..");
const source = (path: string) => readFileSync(join(root, path), "utf8");
test("the CRM migration applies to SQLite, is idempotent, and the seed is repeatable", async () => {
const db = createDb(sqlite());
const migrations = join(root, "app", "db", "migrations");
expect(loadMigrations(migrations).map(({ name }) => name)).toEqual(["001_crm"]);
expect(await migrate(db, migrations)).toEqual(["001_crm"]);
expect(await migrate(db, migrations)).toEqual([]);
await seed(db);
await seed(db);
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_contacts")).toEqual({
count: 2,
});
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_deals")).toEqual({
count: 1,
});
expect(
await db.all(
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('_wrn_authz_assignment','_wrn_authz_grant') ORDER BY name",
),
).toHaveLength(2);
await db.close();
});
test("authentication uses SQL, plugin migrations, signup/login components, and protected routes", () => {
expect(source("app/lib/auth.ts")).toContain("SqlAuthStore");
expect(source("wrnexus.config.ts")).toContain("migrations: true");
expect(source("app/pages/login.wrn")).toContain("<SignIn");
expect(source("app/pages/signup.wrn")).toContain("<SignUp");
expect(source("app/middleware/protected.ts")).toContain('requireAuth({ loginPath: "/login" })');
expect(source("app/pages/dashboard.wrn")).toContain("/api/auth/logout");
expect(source("app/pages/login.wrn")).toContain("AuthSplitLayout");
expect(source("app/pages/login.wrn")).toContain('data-slot="form"');
expect(source("app/pages/signup.wrn")).not.toContain("Already registered?");
expect(source("wrnexus.config.ts")).toContain('entry: "app/styles/global.css"');
});
test("authorization is database-backed and guards both APIs and administration", () => {
expect(source("app/middleware/authz.ts")).toContain("dbPermissionStore(getDb())");
expect(source("app/lib/auth.ts")).toContain('assignRole(user.id, "sales-rep")');
expect(source("app/api/contacts.ts")).toContain('can(ctx, "contact:write")');
expect(source("app/api/deals.ts")).toContain('can(ctx, "deal:read")');
expect(source("app/middleware/protected.ts")).toContain('can(ctx, "admin:access")');
});
test("the app includes public, authentication, and protected CRM pages", () => {
for (const page of [
"index",
"pricing",
"login",
"signup",
"dashboard",
"contacts",
"deals",
"admin",
"forbidden",
]) {
expect(source(`app/pages/${page}.wrn`)).toContain("page ");
}
});
test("protected CRM data is loaded on the server without browser API bindings", () => {
for (const page of ["contacts", "deals", "dashboard"]) {
const contents = source(`app/pages/${page}.wrn`);
expect(contents).toContain("load server");
expect(contents).not.toContain(' api="');
}
expect(source("app/api/dashboard.ts")).toContain("pipeline_value_cents");
expect(source("app/lib/workspace.ts")).toContain("INSERT OR IGNORE INTO crm_contacts");
expect(source("app/lib/auth.ts")).toContain("ensureCrmWorkspace(user.id)");
expect(source("app/middleware/protected.ts")).toContain('new URL("/forbidden", ctx.url)');
});
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": { "noEmit": true },
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
}
+38
View File
@@ -0,0 +1,38 @@
import { join } from "node:path";
import type { AuthConfig } from "@wrnexus/auth";
import type { AppConfig } from "@wrnexus/styles";
import { auth } from "./app/lib/auth.ts";
const config = {
seo: {
title: "Northstar CRM",
titleTemplate: "%s | Northstar CRM",
description: "A complete WrNexus SQLite CRM example.",
canonicalBase: "http://localhost:3000",
},
theme: { palette: "blue", default: "light" },
db: { driver: "sqlite", url: "file:./crm.sqlite" },
styles: {
entry: "app/styles/global.css",
async process({ entryPath, appRoot, mode }) {
const args = ["@tailwindcss/cli", "-i", entryPath!];
if (mode === "production") args.push("--minify");
return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
},
failureMode: "throw",
},
auth: {
engine: auth,
routes: true,
middleware: true,
components: true,
migrations: true,
baseUrl: "http://localhost:3000",
},
profiles: {
test: { db: { driver: "sqlite", url: `file:${join(import.meta.dir, ".tmp-test.sqlite")}` } },
},
security: { cors: { enabled: false } },
} satisfies AppConfig & { auth: AuthConfig };
export default config;
@@ -4,8 +4,6 @@ import { join } from "node:path";
import type { AppConfig } from "@wrnexus/styles"; import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = { const config: AppConfig = {
compatibilityDate: "2026-08-02",
frameworkBehaviour: 1,
// v0.8 defaults: explicit imports, strict template types, safe stores, and // v0.8 defaults: explicit imports, strict template types, safe stores, and
// automatic progressive navigation. Package plugins are discovered from the // automatic progressive navigation. Package plugins are discovered from the
// installed packages above; add custom plugins to this array when needed. // installed packages above; add custom plugins to this array when needed.
@@ -19,14 +17,7 @@ const config: AppConfig = {
checkComponentProps: true, checkComponentProps: true,
generateDeclarations: true, generateDeclarations: true,
}, },
functions: { legacyDefaultRuntime: "current" },
stores: { strictMutations: true, persistence: true }, stores: { strictMutations: true, persistence: true },
compatibility: {
legacyEmit: false,
legacyEventProps: false,
legacyComponentDiscovery: false,
stringLayouts: false,
},
experimental: {}, experimental: {},
performance: { performance: {
@@ -4,8 +4,6 @@ import { join } from "node:path";
import type { AppConfig } from "@wrnexus/styles"; import type { AppConfig } from "@wrnexus/styles";
const config: AppConfig = { const config: AppConfig = {
compatibilityDate: "2026-08-02",
frameworkBehaviour: 1,
// v0.8 defaults: explicit imports, strict template types, safe stores, and // v0.8 defaults: explicit imports, strict template types, safe stores, and
// automatic progressive navigation. Package plugins are discovered from the // automatic progressive navigation. Package plugins are discovered from the
// installed packages above; add custom plugins to this array when needed. // installed packages above; add custom plugins to this array when needed.
@@ -19,14 +17,7 @@ const config: AppConfig = {
checkComponentProps: true, checkComponentProps: true,
generateDeclarations: true, generateDeclarations: true,
}, },
functions: { legacyDefaultRuntime: "current" },
stores: { strictMutations: true, persistence: true }, stores: { strictMutations: true, persistence: true },
compatibility: {
legacyEmit: false,
legacyEventProps: false,
legacyComponentDiscovery: false,
stringLayouts: false,
},
experimental: {}, experimental: {},
performance: { performance: {
+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",
+19 -7
View File
@@ -25,17 +25,17 @@ component SignIn {
class = "" class = ""
} }
view { view {
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'> <section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='wrn-auth-sign-in w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
<header class="mb-6 space-y-1.5"> <header class="wrn-auth-sign-in__header mb-6 space-y-1.5">
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1> <h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p> <p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
</header> </header>
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="space-y-4"> <form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="wrn-auth-sign-in__form">
<input type="hidden" name="returnTo" value='{returnTo}' /> <input type="hidden" name="returnTo" value='{returnTo}' />
<input type="hidden" name="deviceFingerprint" value="" /> <input type="hidden" name="deviceFingerprint" value="" />
<input type="hidden" name="deviceName" value="" /> <input type="hidden" name="deviceName" value="" />
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" /> <Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
<div class="space-y-1.5"> <div>
<TogglePassword <TogglePassword
label="{passwordLabel}" label="{passwordLabel}"
cornerHint="Forgot password?" cornerHint="Forgot password?"
@@ -48,9 +48,7 @@ component SignIn {
size="{size}" size="{size}"
/> />
</div> </div>
{#if showRemember} {#if showRemember}<div class="wrn-auth-sign-in__remember"><Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" /></div>{/if}
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
{/if}
<slot></slot> <slot></slot>
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p> <p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" /> <Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
@@ -64,4 +62,18 @@ component SignIn {
{/if} {/if}
</section> </section>
} }
style {
.wrn-auth-sign-in__header { margin-bottom: 1.5rem; }
.wrn-auth-sign-in__header h1 { font-size: 1.75rem; line-height: 1.15; }
.wrn-auth-sign-in__header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
.wrn-auth-sign-in__form { display: grid; gap: 1.15rem; }
.wrn-auth-sign-in__form > * { margin: 0 !important; }
.wrn-auth-sign-in__remember { margin-top: -0.25rem !important; }
.wrn-auth-sign-in__form .wrn-next__field-heading label,
.wrn-auth-sign-in__form .wrn-next__password-heading { font-size: 0.875rem; font-weight: 650; }
.wrn-auth-sign-in__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
.wrn-auth-sign-in__form [data-error]:empty { display: none; }
.wrn-auth-sign-in__form .wrn-action[data-full-width="true"] { margin-top: 0.15rem !important; }
}
} }
+13 -13
View File
@@ -27,7 +27,7 @@ component SignUp {
<section <section
{...attrs} {...attrs}
data-wrnexus-runtime="auth" data-wrnexus-runtime="auth"
class='w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}' class='wrn-auth-sign-up w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
> >
<header <header
class="mb-6 space-y-1.5" class="mb-6 space-y-1.5"
@@ -50,7 +50,7 @@ component SignUp {
data-schema='{schema}' data-schema='{schema}'
data-redirect='{redirect}' data-redirect='{redirect}'
novalidate novalidate
class="grid gap-4 sm:grid-cols-2" class="wrn-auth-sign-up__form"
> >
<Input <Input
id="auth-sign-up-name" id="auth-sign-up-name"
@@ -61,7 +61,6 @@ component SignUp {
icon="icon-[lucide--user]" icon="icon-[lucide--user]"
color="{color}" color="{color}"
size="{size}" size="{size}"
class="sm:col-span-2"
/> />
<Input <Input
@@ -100,7 +99,6 @@ component SignUp {
icon="icon-[lucide--circle-user-round]" icon="icon-[lucide--circle-user-round]"
color="{color}" color="{color}"
size="{size}" size="{size}"
class="sm:col-span-2"
/> />
{/if} {/if}
@@ -176,15 +174,7 @@ component SignUp {
> >
</p> </p>
<Button <div class="wrn-auth-sign-up__submit"><Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" /></div>
type="submit"
label="{submitLabel}"
variant="solid"
color="{color}"
size="{size}"
fullWidth="true"
class="wrn-auth-sign-up__wide sm:col-span-2"
/>
</form> </form>
<p <p
@@ -196,9 +186,19 @@ component SignUp {
} }
style { style {
.wrn-auth-sign-up > header { margin-bottom: 1.5rem; }
.wrn-auth-sign-up > header h1 { font-size: 1.75rem; line-height: 1.15; letter-spacing: -0.025em; }
.wrn-auth-sign-up > header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
.wrn-auth-sign-up__form { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.15rem; }
.wrn-auth-sign-up__form .wrn-next__field-heading label,
.wrn-auth-sign-up__form .wrn-next--strong-password > label { font-size: 0.875rem; font-weight: 650; }
.wrn-auth-sign-up__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
.wrn-auth-sign-up__form [data-error]:empty { display: none; }
.wrn-auth-sign-up__wide { .wrn-auth-sign-up__wide {
grid-column: 1 / -1; grid-column: 1 / -1;
min-width: 0; min-width: 0;
} }
.wrn-auth-sign-up__submit { width: 100%; margin-top: 0.25rem; }
.wrn-auth-sign-up__submit .wrn-btn { min-height: 3rem; font-size: 0.9375rem; }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/auth", "name": "@wrnexus/auth",
"version": "0.8.12", "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,
+34 -10
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 {
@@ -160,10 +161,6 @@ export interface AuthEngine {
key: string; key: string;
response: unknown; response: unknown;
name?: string; name?: string;
/** @deprecated Verification uses the RP ID bound to the issued challenge. */
rpId?: string;
/** @deprecated Verification uses the origin bound to the issued challenge. */
origin?: string;
}, },
): Promise<boolean>; ): Promise<boolean>;
beginPasskeyAuthentication(input: { beginPasskeyAuthentication(input: {
@@ -176,10 +173,6 @@ export interface AuthEngine {
response: unknown; response: unknown;
/** Request metadata used only for the resulting session. */ /** Request metadata used only for the resulting session. */
session?: Partial<AuthSession>; session?: Partial<AuthSession>;
/** @deprecated Verification uses the RP ID bound to the issued challenge. */
rpId?: string;
/** @deprecated Verification uses the origin bound to the issued challenge. */
origin?: string;
}): Promise<AuthResult>; }): Promise<AuthResult>;
changePassword( changePassword(
userId: string, userId: string,
@@ -274,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");
@@ -832,7 +847,7 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
store, store,
onSignedIn: options.onSignedIn, onSignedIn: options.onSignedIn,
onSignedOut: options.onSignedOut, onSignedOut: options.onSignedOut,
onSuccessfulSignUp: options.onSuccessfulSignUp ?? options.onSuccessfullSignUp, onSuccessfulSignUp: options.onSuccessfulSignUp,
async register(input) { async register(input) {
try { try {
@@ -1657,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();
@@ -2284,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;
} }
+97 -13
View File
@@ -9,11 +9,9 @@ import {
getAuthUser, getAuthUser,
} 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 { import type { AuthSessionVerificationHandler } from "../types.ts";
AuthSessionVerificationHandler, import { completeAuth, startAuth, type OAuthProvider } from "@wrnexus/oauth";
AuthSignedInHandler, import { assignDefaultAuthzRoles } from "@wrnexus/authz/defaults";
AuthSignedOutHandler,
} from "../types.ts";
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);
@@ -53,20 +51,29 @@ export interface AuthHttpOptions {
baseUrl?: string; baseUrl?: string;
schemas?: AuthSchemaOverrides | AuthSchemaSet; schemas?: AuthSchemaOverrides | AuthSchemaSet;
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
/** @deprecated Prefer createAuthEngine({ onSignedIn }). */
onSignedIn?: AuthSignedInHandler;
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
onSignedOut?: AuthSignedOutHandler;
onSessionVerification?: AuthSessionVerificationHandler; onSessionVerification?: AuthSessionVerificationHandler;
oauth?: Record<string, OAuthProvider>;
oauthMfaPath?: string;
} }
export function createAuthHttpHandlers(options: AuthHttpOptions) { export function createAuthHttpHandlers(options: AuthHttpOptions) {
const engine = options.engine; const engine = options.engine;
const schemas = resolveAuthSchemas(options.schemas); const schemas = resolveAuthSchemas(options.schemas);
const onSignedIn = options.onSignedIn ?? engine.onSignedIn; const onSignedIn = engine.onSignedIn;
const onSignedOut = options.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);
@@ -88,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) {
@@ -118,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) {
@@ -238,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);
}, },
@@ -411,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,
@@ -429,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;
} }
+70 -17
View File
@@ -2,13 +2,16 @@ 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 { import type { AuthSessionVerificationHandler } from "./types.ts";
AuthSessionVerificationHandler,
AuthSignedInHandler,
AuthSignedOutHandler,
} from "./types.ts";
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts"; import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
import { import {
clearDefaultAuthEngine, clearDefaultAuthEngine,
@@ -36,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;
@@ -52,16 +58,36 @@ export interface AuthConfig {
baseUrl?: string; baseUrl?: string;
csrf?: boolean; csrf?: boolean;
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
/** @deprecated Prefer createAuthEngine({ onSignedIn }). */ oauth?: Partial<Record<"google" | "github" | "discord", AuthOAuthProviderConfig>> &
onSignedIn?: AuthSignedInHandler; Record<string, AuthOAuthProviderConfig>;
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */ oauthMfaPath?: string;
onSignedOut?: AuthSignedOutHandler; /** 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;
@@ -94,10 +120,10 @@ interface ResolvedAuthConfig {
baseUrl?: string; baseUrl?: string;
csrf: boolean; csrf: boolean;
passkey?: AuthPasskeyHttpOptions; passkey?: AuthPasskeyHttpOptions;
onSignedIn?: AuthSignedInHandler;
onSignedOut?: AuthSignedOutHandler;
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));
@@ -131,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,
@@ -156,10 +200,10 @@ function resolveConfig(
baseUrl: raw.baseUrl, baseUrl: raw.baseUrl,
csrf: raw.csrf ?? true, csrf: raw.csrf ?? true,
passkey: raw.passkey, passkey: raw.passkey,
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
sessionCookieName: raw.sessionCookieName, sessionCookieName: raw.sessionCookieName,
onSessionVerification: raw.onSessionVerification, onSessionVerification: raw.onSessionVerification,
oauth,
oauthMfaPath: raw.oauthMfaPath,
}; };
} }
@@ -188,6 +232,7 @@ function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
schemas: resolveAuthSchemas(), schemas: resolveAuthSchemas(),
csrf: true, csrf: true,
oauth: {},
}; };
} }
@@ -365,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);
@@ -392,13 +447,11 @@ export function authPlugin(options: AuthPluginOptions = {}) {
passkey: value.passkey, passkey: value.passkey,
onSignedIn: value.onSignedIn,
onSignedOut: value.onSignedOut,
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 -2
View File
@@ -60,9 +60,9 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
schemas: getDefaultAuthSchemas(), schemas: getDefaultAuthSchemas(),
baseUrl: routeOptions.baseUrl ?? ctx.url.origin, baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
passkey: routeOptions.passkey, passkey: routeOptions.passkey,
onSignedIn: routeOptions.onSignedIn,
onSignedOut: routeOptions.onSignedOut,
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);
}

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