Compare commits

..
Author SHA1 Message Date
ClintchizandClaude Opus 5 361129b6ac perf(build): budget the runtime on what ships, not on source bytes
The runtime budgets measured raw source, which counts comments -- and the
production build minifies, so comments cost a visitor nothing. The metric
therefore rewarded deleting explanatory comments over writing smaller code,
and could not tell a real feature from a wall of prose.

They now measure the minified output, which is what is actually served:
/__wrnexus/reactive.js is its own file, minified, with an immutable year-long
cache. The reactive runtime is 69684 minified against a 80000 budget, from
175246 raw -- roughly 21kB gzipped, fetched once.

Also fixes two real bugs found while testing the showcase:

  - object-valued props were serialised as a bare {...} attribute, which the
    compiler read as interpolation and tried to parse as JavaScript. That
    returned 500 for /components/navbar. Arrays start with [ and were never
    affected, which is why only object props broke. All 108 pages now render.

  - the runtime walked text nodes inside textarea, script and style, so a
    JSON sample in a textarea was evaluated away.

Navbar styles move out of ui.css into the component, matching the rest of the
navigation group. No declarations changed: 4519 before and after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:20:19 +05:30
ClintchizandClaude Opus 5 8069541cd9 refactor(csr): one document observer with subscribers
Overlay clamping, dialog focus, roving focus and scrollspy each ran their own
MutationObserver over the same stream of records. They now share one, with the
per-feature work registered as subscribers. Every subscriber already defers,
so the extra callbacks are cheap and the bookkeeping is paid for once.

The attribute filter stays explicit rather than observing everything: an
unfiltered observer would see the tabindex the roving code writes and loop on
its own output.

This is better structured but it is not a fix for the size budget -- it buys
82 bytes of headroom, not room to grow. Splitting the runtime so a page pays
only for the behaviour it uses is still the outstanding decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:26:57 +05:30
ClintchizandClaude Opus 5 4a82640c5b merge: navigation components phase 3
Quality / quality (ubuntu-latest) (push) Failing after 12m47s
Quality / quality (windows-latest) (push) Canceled after 0s
Scrollspy built, Navbar given roving focus, and the invalid empty aria-current
fixed across Navbar and Breadcrumb.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:22:10 +05:30
ClintchizandClaude Opus 5 52660cbb8e feat(ui): build Scrollspy, fix aria-current across the navigation group
Scrollspy replaces a scaffold that rendered bare anchors. The runtime observes
the sections the links point at and writes the marker straight onto the links:
an IntersectionObserver callback fires long after the client function that
registered it returned, so a state write there would be dropped.

Navbar and Breadcrumb both emitted aria-current="" for every inactive link.
That is not a valid value -- the attribute takes a token or must be absent --
so every link claimed a state it did not have. Breadcrumb had it too, despite
being the strongest component in the group.

Navbar also takes roving arrow-key focus across its menu bar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 19:18:25 +05:30
ClintchizandClaude Opus 5 ca2f9451ab merge: navigation components phase 1 and 2
Quality / quality (ubuntu-latest) (push) Failing after 12m45s
Quality / quality (windows-latest) (push) Canceled after 0s
Roving arrow-key focus in the runtime, then Nav, Pagination, Stepper,
MegaMenu, a Sidebar rebuilt on Drawer, and a Tabs rewritten off Tailwind onto
wire classes with real outputs and url-backed selection.

Also fixes the modal focus trap shipped in 0.8.5, which gated on
getBoundingClientRect and so never ran under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:54:26 +05:30
ClintchizandClaude Opus 5 b3116de354 fix(ui): derive tab selection from the url instead of syncing to it
My diagnosis in the previous commit was wrong. The component root was not
being replaced by a reactive re-render: the client router owns popstate and
swaps the whole page shell on back and forward, which discards component
state entirely. Every mechanism that tried to push state into the component
from outside was therefore doomed -- clicking a tab, announcing an event,
tracking the last applied value.

In url mode the query parameter is now simply the source of truth, read where
the selection is computed. Whatever render happens next produces the right
tab, with no listener to lose and nothing to keep in step.

This deletes the runtime tab sync entirely -- 1590 bytes -- and fixes the
back/forward cases that were previously broken. Verified in the showcase:
click writes the url, two backs and two forwards each land on the right tab,
and a ?tab= deep link opens on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:54:13 +05:30
ClintchizandClaude Opus 5 f6993e6cdb fix(csr): drive tab url restore by announcement, anchor nav submenus
Two mechanisms tried and rejected while testing this against the live
showcase, both failing the same way on a second history step:

  - synthesising a click on the matching tab: a re-render replaces the tab
    buttons, and clicking a freshly replaced node that has not been bound
    does nothing at all
  - tracking the last applied value in the runtime: that state drifts out of
    step with the component and silently swallows real changes

The runtime is now stateless. It announces the value the URL names via a
wrnexus:tabs:restore event and the component applies it, comparing against
its own selection rather than a DOM attribute a re-render owns.

Nav submenus are anchored so the viewport clamp keeps them on screen.

Comments in the runtime template trimmed to stay inside the size budget
rather than raising the ceiling again.

Known limitation: a second consecutive back/forward does not update the
selection, because the re-render replaces the component root without
rebinding its declarative listeners. That is a framework defect, not a Tabs
one, and needs its own fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:47:41 +05:30
ClintchizandClaude Opus 5 f1d1081b67 feat(ui): build MegaMenu and rebuild Sidebar on Drawer
MegaMenu replaces a scaffold with a trigger and a wide panel of grouped link
columns. One level deep on purpose: a mega menu exists to show breadth flat so
everything is one click away, and nesting inside the panel buries content
behind hover-within-hover. Nav is the component for cascading submenus. The
panel is anchored so the runtime clamp keeps it inside the viewport.

Sidebar now composes Drawer for its off-canvas presentation instead of a
hand-rolled backdrop, inheriting the focus trap and scroll lock from one
place. Single items, labelled groups and branches nested to three levels, with
vertical roving focus.

Sidebar classes move to the BEM naming the rest of the library uses, which is
a breaking change; nesting via children still works, since that is what
shipped in 0.8.5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:24:03 +05:30
ClintchizandClaude Opus 5 b4d3cb3695 feat(ui): rewrite Tabs onto wire classes with URL sync and roving focus
Tabs was the only component in the library styled with Tailwind utilities, so
it could not be themed like the rest and assumed Tailwind was present. It also
fired raw CustomEvents instead of declaring outputs, and set a roving tabindex
with no keydown handler at all -- which left every inactive tab unreachable by
Tab while the arrows did nothing.

It now uses wire-* classes and a local style block, declares change and select
outputs, and opts into the roving runtime.

mode=url mirrors the selection into a query parameter via pushState. Back and
forward are handled in the runtime, which activates the matching tab rather
than assigning to component state: a popstate listener writing state would be
writing after the client function returned, and that write is dropped. The
round trip is marked so the component does not push a second history entry for
a navigation that came from history.

Also anchors Nav submenus so the viewport clamp can pull them back on screen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:18:00 +05:30
ClintchizandClaude Opus 5 124da548b8 chore(ui): wire color and size into the new navigation components
Every bundled component must expose color and size; the rewrites dropped
them, which the library-wide invariant test caught. Rather than re-adding
them as dead props, each component now maps color onto an accent variable
that its active, current and focus affordances actually use, and size onto
the root font scale.

Regenerates the component reference, showcase and visual contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:57:10 +05:30
ClintchizandClaude Opus 5 f4960f2fc5 feat(ui): build the Nav component with submenus and roving focus
Replaces a scaffold that rendered bare anchors. Flat or nested to three
levels, icons, badges, disabled items, aria-current on the active link,
arrow-key roving focus, and a disclosure arrow that rotates on open.

Three levels rather than arbitrary depth because this template language has
no component recursion, so each level is written out.

On a phone the bar becomes a toggle and submenus stack inline rather than
floating: a hover-opened overlay cannot be reached on touch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:47:20 +05:30
ClintchizandClaude Opus 5 45ef035bae feat(ui): build the Stepper component
Replaces a scaffold that rendered bare anchors with ordered steps: complete,
current and upcoming status derived from the active index, horizontal or
vertical, optional icons, and indexed named slots (step-0, step-1, ...) for
authoring a step body by hand.

Also tightens the roving contract from the previous commit. A template writes
data-wrn-roving="" or data-wrn-roving-item="false" to mean not this time, but
a bare [attr] selector matches either, so a read-only stepper would still have
taken arrow-key focus. The container now requires a named axis, while a bare
item marker still counts as opted in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:44:12 +05:30
ClintchizandClaude Opus 5 b67a5e43eb feat(ui): build the Pagination component
Replaces a scaffold that rendered a bare list of anchors with real page
controls: compact arrows or windowed page numbers, a range summary, and a
change output carrying the requested page. Out-of-range pages clamp rather
than rendering nothing, because page arrives as an HTML attribute and callers
compute it from data that may have shrunk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:41:56 +05:30
ClintchizandClaude Opus 5 7ac6e08544 fix(csr): make dialog visibility testable and cover the focus trap
The focus trap and scroll lock shipped in 0.8.5 gated on
getBoundingClientRect, which the test DOM always reports as zero, so a dialog
never counted as open and none of that behaviour ran under test. focusableWithin
had the same measurement gate and would have found no items even once the
visibility check was fixed.

Both now use the hidden attribute and the data-show marker the components
already emit. Behaviour in a real browser is unchanged; the difference is that
it is now covered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:40:30 +05:30
ClintchizandClaude Opus 5 f94004648d feat(csr): runtime-owned roving arrow-key focus
A container marked data-wrn-roving owns its [data-wrn-roving-item]
descendants: one carries tabindex=0 so Tab reaches the group once, and the
arrow keys move within it, with Home/End, wrap-around and skip-disabled.

Written once here rather than five times across Tabs, Nav, MegaMenu, Sidebar
and Stepper, and because focus bookkeeping cannot live in component state --
a client function writing after it returns has that write dropped.

Item visibility is checked via hidden and data-show rather than measured
size: the test DOM reports every element as zero-sized, which is exactly what
left the dialog focus trap uncovered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:38:44 +05:30
ClintchizandClaude Opus 5 e8e1a2623b docs: navigation phase 1 implementation plan
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:35:42 +05:30
ClintchizandClaude Opus 5 ecb93c7116 docs: navigation component group design
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:28:30 +05:30
ClintchizandClaude Opus 5 f01a308287 chore(release): stage 0.8.5 package tarballs
Quality / quality (ubuntu-latest) (push) Failing after 12m26s
Quality / quality (windows-latest) (push) Canceled after 0s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:16:43 +05:30
ClintchizandClaude Opus 5 1fb1a8d2d0 chore(release): prepare 0.8.5
Quality / quality (ubuntu-latest) (push) Failing after 12m21s
Quality / quality (windows-latest) (push) Canceled after 0s
Bumps every @wrnexus package 0.8.4 -> 0.8.5 and adds the matching update
migration. The migration is documentation only: moving off <Table> to
<DataTable> and off the @wrnexus/ui main entry to @wrnexus/ui/registry are
source changes no codemod can make safely.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:08:05 +05:30
ClintchizandClaude Opus 5 949cf78636 feat(ui): add DataTable and Toaster, drop the legacy Table, fix overlay dialogs
Quality / quality (ubuntu-latest) (push) Failing after 13m40s
Quality / quality (windows-latest) (push) Canceled after 0s
DataTable replaces the 20-line Table scaffold entirely: columns, sorting,
filtering, pagination, selection, bulk actions, comparison layout, sticky
first column, custom HTML cells, and a remote source driven by a `request`
output rather than a function prop (props travel as HTML attributes, so a
function arrives as its own source text).

Toaster replaces the hand-rolled status div: tone icons, actions, hover
pause/resume and a progress bar.

Overlays audit -- Modal and Drawer declared aria-modal="true" but nothing
ever moved focus into the panel, so the @keydown handler on their root
never ran and closeOnEscape did nothing. Focus, focus restore, a Tab trap
and a body scroll lock now live in the reactive runtime, shared by both.

ContextMenu placed pointer menus by subtracting a guessed 340x420 from the
viewport, which pushed every menu that was not that size away from the
pointer; it now positions at the pointer and lets the anchored clamp pull
it back once it can be measured.

The reactive runtime size budget moves 150k -> 175k to cover anchored
overlays, dialog behaviour, the toaster and the DataTable client half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 14:59:58 +05:30
Clintchiz 296728d51d merge: inter-app RPC 2026-08-05 21:40:30 +05:30
Clintchiz 98205daef6 fix(rpc): isolate integration test from cross-suite fetch pollution
packages/csr's actions.test.ts and reactive.test.ts both leave
globalThis.fetch mutated across bun test files (reactive.test.ts's
'cache invalidation refetches...' test replaces it and never restores
it). Since bun test runs files sequentially rather than importing all
of them up front, a module-level capture of fetch in this file would
already observe csr's leftover mock (csr sorts before rpc).

Route the real-socket assertion through a small node:http-backed fetch
implementation instead of relying on globalThis.fetch at all, keeping
the test's actual target - httpTransport()'s default resolveOrigin -
unaffected by any other suite's global mutation.
2026-08-05 20:57:08 +05:30
ClintchizandClaude Opus 5 3eec9fd8c6 fix(rpc): close the four final-review blockers on inter-app RPC
- Resolve RPC call origins via a new WRNEXUS_INTERNAL_ORIGINS map (loopback
  origins the gateway hands each child before spawning it), falling back to
  the public appOrigin only when it is absent. Calls previously always went
  to the public gateway origin, which the gateway unconditionally 404s on
  the RPC prefix by design — every real cross-app call failed.
- Stop loadServices() from running ahead of routing and stop memoizing a
  rejected load: one bad file under app/services/ no longer permanently
  breaks every route in the app. A failed load logs loudly, is retried on
  the next RPC request, and the RPC path gets a structured RPC_UNKNOWN
  instead of an unhandled throw.
- Reject a service whose contract.name does not match the filename it is
  mounted under, naming both, instead of silently mounting under the
  filename while the typed client calls by contract name.
- Let ServiceError accept an explicit retryable and have the client pass the
  wire value through, instead of recomputing (and silently flipping) it from
  the error code alone.
- Document the gateway/X-Forwarded-* deployment requirement in the RPC
  README.

Each of the three code blockers has a new/extended test that was verified to
fail when its fix was reverted (rpc/test/integration.test.ts,
dev-server/test/rpc-services-loading.test.ts).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:36:31 +05:30
ClintchizandClaude Opus 5 6aaf21aa06 feat(rpc): add the caller-side example and close remaining coverage gaps
Adds examples/auth-showcase/app/services/greeter-client.ts so the showcase
demonstrates both halves - the review noted the example was callee-only, so a
developer had no working reference for making a call.

Raises integration coverage to the planned 3 tests and adds the missing
rpc-endpoint cases. Also wires the prod build path for services.

304 tests pass across rpc/router/dev-server/cli; typecheck, lint, format and
check:public-api all clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:21:41 +05:30
ClintchizandClaude Opus 5 ce68803471 fix(rpc): close the service-collision fail-open and the fix-wave gaps
Critical:
- router: fail loudly (WRN-SERVICE-COLLISION) when two app/services files
  scan to the same service name, instead of silently letting directory-walk
  order pick a winner.

Important:
- server.ts: wrap a throwing input schema so its raw message cannot escape
  invoke(); returns RPC_INVALID and logs server-side instead.
- client.ts: race timeoutMs against transport.call so a stalled transport
  cannot hang the caller; rejects with a ServiceError(RPC_TRANSPORT).
- client.ts: the proxy returns undefined for undeclared properties (incl.
  then/catch/finally) instead of a function that throws, closing the
  await-client thenable trap.
- gateway.ts / rpc-dispatch.ts: import RPC_PATH_PREFIX / RPC_INTERNAL_HEADER
  from @wrnexus/rpc instead of hardcoding local copies.
- gateway.test.ts: cover the RPC-prefix edge block and internal-header
  stripping across casing variants.
- http.test.ts / client.test.ts: cover anonymous-call header omission, the
  internal marker, the retryable-status sweep, network/malformed/HTML
  failures, AbortSignal propagation, the timeout path, and timer cleanup.

Minor:
- transport.ts: Object.hasOwn for handler lookup; note the entry-only abort
  check.
- client.ts: wrap a missing/invalid WRNEXUS_RPC_SECRET as a ServiceError
  (RPC_IDENTITY) instead of a bare Error.
- rpc/package.json: drop the unused @wrnexus/authz dependency.
- server.ts: implement() now throws at construction time if a declared
  procedure has no own handler.

Verified: reverting the service-collision check and the client timeout race
each make their new test fail, then restore green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:10:20 +05:30
ClintchizandClaude Opus 5 7c4b484d0a fix(rpc): close prototype-chain permission bypass, add server/client/transport tests
C1 CRITICAL: implement() looked up procedures/handlers with plain property
indexing, so any Object.prototype member name (constructor, toString, etc.)
resolved truthy and skipped the permission gate entirely. Fixed with
Object.hasOwn checks in packages/rpc/src/server.ts. Defense-in-depth guard
added in packages/dev-server/src/rpc-dispatch.ts constraining URL path
segments to a safe charset before they reach service/procedure lookups.

Added missing direct test coverage for packages/rpc/src/transport.ts,
server.ts and client.ts (previously untested), including a prototype-name
sweep in both server.test.ts and dev-server's rpc-endpoint.test.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:46:59 +05:30
ClintchizandClaude Opus 5 fcf4ed3039 docs: close a prototype-chain authorization bypass in the plan
server.ts looked procedures up with plain property indexing, so every
Object.prototype member resolved as truthy. A prototype member carries no
`permission`, so the permission gate was skipped entirely.

Verified: with a contract whose only procedure declares a permission and a
checkPermission that always denies, invoke("add") correctly returns
RPC_DENIED, while invoke("constructor") returns {"ok":true,"value":{"a":2}}
and the gate never runs.

Reachable over the wire as POST /__wrnexus/rpc/<service>/constructor by
anything that clears the internal-caller check - i.e. any workspace app.

Fixed at both layers: Object.hasOwn for the procedure and handler lookups,
and a character-class guard on the path segments before they are used as
lookup keys.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:42:49 +05:30
ClintchizandClaude Opus 5 e01915823a feat(rpc): transport, server, client, http, mounting, docs (Tasks 5-11)
Brings the uncommitted body of work under version control so it cannot be
lost. Gates are green: 152 tests pass across rpc/router/dev-server,
typecheck, lint, format and check:public-api all clean.

NOT YET REVIEWED. None of Tasks 5-11 has had an independent task review, and
Task 4's second fix round was never re-reviewed either.

Known gaps against the plan, recorded here rather than discovered later:
- packages/rpc/test/{transport,server,client}.test.ts are ABSENT. The plan
  required a test file for each. server.ts holds the fail-closed identity and
  permission checks and currently has no direct coverage at all.
- rpc-endpoint.test.ts has 3 tests where the plan specified 9. Missing:
  unknown service, non-POST, malformed body, non-rpc passthrough, and the
  isInternalCaller sweep. This is the task where a reachable
  /__wrnexus/rpc/* makes every permission check in the workspace bypassable.
- http.test.ts has 3 of 7; integration.test.ts 2 of 3;
  services-discovery.test.ts 1 of 4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:38:04 +05:30
ClintchizandClaude Opus 5 9bc0f48514 fix(rpc): close the iat fail-open and tighten the identity guards
verifyJwt gates its maxAge check on iat being a number, so a token forged
without iat was honoured at any maxAgeSeconds - the same shape as the
audience and exp fail-opens closed in the previous round. A future-dated iat
did the same via a negative age. Both refused now.

The import side never checked aud was a single string, and verifyJwt compares
with includes(), so a multi-audience token verified at several apps. The
mint-side guard's invariant now holds where it is enforced.

ctx.tenant present with a null id minted an authenticated credential with no
tenant claim, which the callee reads as global. Absent ctx.tenant means
untenanted; a present tenant with an unusable id is an error.

Adds six tests pinning behaviours that mutation testing showed were free to
delete without any test noticing: no-exp, no-iat, the 300s default max age,
an array audience on import, a non-string tenant claim on import, and a null
tenant id at mint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:32:41 +05:30
ClintchizandClaude Opus 5 9f599e02e8 docs: close the iat fail-open and tighten the Task 4 identity guards
The round-1 fix required exp and passed maxAge, but verifyJwt gates its age
check on iat being a number - the identical shape to the two fail-opens that
round closed. A token minted without iat defeats the age bound at ANY
maxAgeSeconds, and a future-dated iat yields a negative age and does the
same. Both refused now, so maxAge means what ImportOptions says it means.

The mint side refused an array targetApp, but the import side never checked
that aud was a single string, and verifyJwt compares with includes(). So a
multi-audience token still verified at several apps - the invariant was true
only where it was not enforced. Now checked at the callee.

ctx.tenant present with a null id was treated as untenanted, silently
widening scope to global while still issuing an authenticated credential.
Absent ctx.tenant means global; a present tenant with an unusable id is an
error.

Also exports ImportOptions, which the append snippet omitted although the
Produces line names it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:36:05 +05:30
ClintchizandClaude Opus 5 83c99cc3e5 fix(rpc): close identity-token fail-open and validation gaps
- importSubjectContext now rejects a non-string/empty selfApp before
  verifying. verifyJwt skips the audience check entirely when audience
  is undefined, so an unvalidated selfApp (the natural shape of
  currentAppName(): string | undefined) accepted every token from every
  app for every audience.
- exportSubjectContext now rejects a non-string/empty targetApp, so an
  array can no longer mint one token valid at multiple apps.
- Both directions now reject a present-but-non-string tenant id instead
  of silently dropping it (was: callee reads missing tenantId as
  global/unscoped -> cross-tenant exposure).
- importSubjectContext now requires exp to be present and independently
  bounds accepted token age via a new maxAge/ImportOptions.maxAgeSeconds
  (default 300s), so a caller cannot mint a long-lived token via a huge
  ttlSeconds and have it honoured indefinitely.
- SubjectContext.callerApp doc now states it is self-asserted (the
  signing secret is workspace-wide) and must never be an authz input.
- index.ts also exports the new ImportOptions type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:26:14 +05:30
ClintchizandClaude Opus 5 1393a8a3b8 docs: close a fail-open and three gaps in the Task 4 identity plan
CRITICAL: importSubjectContext never validated selfApp, and verifyJwt skips
the audience check entirely when audience is undefined. So an undefined
selfApp disabled the only cross-app binding in the system and accepted every
token from every app for every audience. Not hypothetical - the natural feed
is helpers' currentAppName(), which returns string | undefined. The mint side
already hard-fails on a missing app name; the import side did not.

A non-string tenant id was silently dropped at both ends. A numeric tenant id
is the common DB-backed case, and a callee reading a missing tenantId as
"global" is a cross-tenant exposure. Now refused, symmetric with the subject
check.

Token lifetime was unbounded: verifyJwt only checks exp when present, so a
token minted without one never expired, and a caller passing a large
ttlSeconds produced a long-lived impersonation credential the callee
honoured. exp is now required and age is bounded by maxAge independently.

targetApp was unvalidated, so passing an array minted one token valid at
several apps - exactly what the audience binding exists to prevent.

Also documents callerApp as self-asserted rather than authenticated
provenance, since the signing secret is workspace-wide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:21:38 +05:30
ClintchizandClaude Opus 5 2257ee871e feat(rpc): add the signed subject-context token
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:12:42 +05:30
Clintchiz 40625e98ed fix(rpc): deep-freeze procedures in defineService, not just the map 2026-08-05 14:03:40 +05:30
ClintchizandClaude Opus 5 e0bd84247e docs: deep-freeze procedures in the Task 3 plan snippet
defineService froze the procedures map but not each procedure inside it, so a
ProcedureDef built by hand rather than through procedure.build() stayed
mutable: svc.procedures.foo.permission = 'hacked' silently succeeded. The
contract is shared between two apps as a single source of truth, and the
guarantee rested on every call site remembering to use the builder.

Same class as the authz catalog's frozenMap, which froze the Map's mutators
but not the values it handed out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 14:01:52 +05:30
ClintchizandClaude Opus 5 34d5bbc810 docs: add the missing cast to .input() in the Task 3 plan snippet
The builder's .input() did not typecheck as written (TS2345). The phantom
__input/__output markers make ProcedureDef invariant, which is exactly why
.output<T>() already carried a cast - .input() needed the analogous one and
did not have it.

Caught by the Task 3 implementer, who also verified via @ts-expect-error that
InferProcedureInput/InferProcedureOutput genuinely reject wrong shapes, so
the phantom markers are carrying real type information rather than silently
widening.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:54:23 +05:30
Clintchiz e16903b286 feat(rpc): add defineService and the immutable procedure builder 2026-08-05 13:53:14 +05:30
ClintchizandClaude Opus 5 21ea8a84a0 fix(rpc): bound retryable status range and add malformed-response code
- isRetryableStatus now fails closed for out-of-range values (600+, negative,
  NaN) by bounding the 5xx check on both sides (>= 500 && <= 599), instead of
  an unbounded >= 500 that classified garbage statuses like 1000 as retryable.
- 408 Request Timeout is now retryable, matching the RPC_TRANSPORT doc
  comment (connection, timeout, 5xx) — a timeout surfaced as 408 is no longer
  treated differently from the same timeout surfaced as 504.
- Add RPC_MALFORMED: the callee answered, but not with a ServiceResult (HTML
  error page, truncated body, unexpected shape). Distinct from RPC_TRANSPORT
  since something DID respond; non-retryable via the existing retryableFor,
  no new branch needed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:45:37 +05:30
ClintchizandClaude Opus 5 dd8354477d docs: close three retryability gaps in the Task 2 plan snippet
isRetryableStatus used an unbounded status >= 500, so a garbage status like
1000 landed in the retryable bucket. This function is the sole gate the
client and HTTP transport trust for retry safety, and an out-of-range value
must fail closed. Bounded on both sides.

408 Request Timeout was non-retryable while the same file documented
transport as covering "connection, timeout, 5xx" - a genuine timeout
surfaced as 408 was classified differently from the identical timeout
surfaced as 504. Now retryable.

There was no code for "the callee answered but not with a ServiceResult" - a
proxy's HTML error page, a truncated body. Task 8 was already papering over
it by hand-setting retryable: false beside a transport code that
retryableFor says is always retryable, which is exactly how the two drift
apart. Added RPC_MALFORMED and made that path use failure() so retryability
is derived from the code rather than written next to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:43:39 +05:30
ClintchizandClaude Opus 5 796b19d923 feat(rpc): add service errors and retryability classification
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:37:52 +05:30
Clintchiz 3e1d7db537 fix(rpc): resolve lint warnings from review follow-up
- Drop the redundant eslint-disable on AnyProcedures; no-explicit-any
  is off repo-wide so the directive itself was the warning. Doc
  comment now explains why none is needed.
- Rename test's schema binding to _schema per the lint config's
  underscore-prefix rule for read-only-as-type bindings.
2026-08-05 09:59:23 +05:30
ClintchizandClaude Opus 5 a4c7d7b298 docs: drop a redundant eslint directive and note a Bun test quirk
no-explicit-any is off repo-wide in eslint.config.js, so the disable comment
the plan mandated is itself an unused-directive warning. The test's schema
binding also needs the _ prefix the lint config requires for a value read
only via typeof.

Separately: bun test strips type-only imports before resolution, so the
red-first step does not reproduce for type-only tests. Recorded so later
implementers do not chase it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:54:08 +05:30
Clintchiz e1fca3eddf feat(rpc): scaffold the package and shared contract types 2026-08-05 09:52:48 +05:30
ClintchizandClaude Opus 5 63e6148cdb docs: implementation plan for the inter-app communication system
Eleven TDD tasks covering phase 1: contract and immutable procedure builder,
error classification, the signed subject-context token, the Transport seam
with an in-process transport for tests, implement() with fail-closed identity
and permission checks, the typed client proxy, the HTTP transport, router
discovery of app/services, and the mounted endpoint with its two independent
external-access guards.

Phases 2-4 (retry and circuit breaking, app-to-app streaming, identity for
pubsub and queue) are documented as deferred with the reason each needs its
own design pass.

Task 10 is called out as the highest-risk: if /__wrnexus/rpc/* is reachable
from the public internet, every permission check in the workspace is
bypassable, so the plan requires the gateway block and the app-side check to
be verified as working independently of each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:37:39 +05:30
ClintchizandClaude Opus 5 87d33a2ce1 docs: design for the inter-app communication system
Typed request/response between workspace apps over HTTP, behind a Transport
seam so gRPC stays additive rather than a rewrite. Contracts live in the
workspace's shared package and are imported by both sides, so types flow
through a normal import with no code generator.

Consumes the exportSubjectContext/importSubjectContext seam the permissions
system reserved, with one improvement on what that seam implied: the token
carries sub and tenant only, never roles. Every app shares the
PermissionStore, so the callee resolves roles itself - a stale or forged
roles claim becomes impossible by construction and there is no path to
injecting privileges through a claim. The token authenticates; it never
authorizes.

Records two properties that are easy to get wrong and expensive to discover:
/__wrnexus/rpc/* must be unreachable from the public internet, blocked at the
gateway AND verified at the app, or every permission check in the workspace
is bypassable; and only procedures explicitly marked idempotent may be
retried, because retrying a slow createInvoice is how a customer gets billed
twice.

Deliberately does not wrap pubsub or queue - they work, and an abstraction
over working code leaks and needs keeping in sync. They gain identity
propagation instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 09:27:43 +05:30
ClintchizandClaude Opus 5 2ab7b1e762 chore: sync the lockfile with the authz workspace dependencies
packages/authz gained @wrnexus/core and @wrnexus/db, and
examples/auth-showcase gained @wrnexus/authz, but no install ran afterwards
so bun.lock never recorded them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:41:22 +05:30
ClintchizandClaude Opus 5 2e93124080 merge: 0.8.4 security audit fixes and the permissions system
Two bodies of work, both reviewed before merge.

SECURITY AUDIT of 0.8.4. The repo's own gates were already green, so every
finding came from manual review and each was reproduced before being claimed:
safeFetch re-attached credentials after a cross-origin redirect; its
private-network guard was advisory only and defeated by DNS rebinding; three
IPv6 forms bypassed the private-address check; sanitizeUrl returned
protocol-relative input verbatim (open redirect); the gateway threw on
malformed Basic credentials, truncated passwords at the first colon, and
leaked password length by timing; RBAC namespace wildcards matched only the
first segment; the brace-expansion override was pinned to the exact
vulnerable version.

PERMISSIONS SYSTEM in @wrnexus/authz. Declaration catalog discovered from
app/authz, a pluggable PermissionStore with memory and sqlite adapters held
to one 24-test conformance suite, a resolution engine with deny-wins
precedence and fail-closed error handling, request middleware, an audit sink,
type codegen, a wrnexus authz CLI, and dev/prod boot wiring.

Behaviour changes needing release notes: authorizeDecision's 403 body no
longer carries reason or policy (opt back in with exposeReason); RBAC
wildcards now match at every depth, which widens access for anyone relying on
the old behaviour; Router gained a required authz field; subject.id must be a
non-empty string. See docs/plans/2026-08-05-authz-follow-ups.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 08:37:55 +05:30
ClintchizandClaude Opus 5 2c339bee15 docs: record the adjudicated non-blocking authz findings
Findings from the task and whole-branch reviews that were ruled non-blocking,
plus the behaviour changes that need release notes. None is an authorization
bypass. Recorded in the repo because the review workspace is scratch and git
history does not carry the reasoning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:20:39 +05:30
ClintchizandClaude Opus 5 41b6e2ed2b fix(authz): freeze catalog values after boot; correct compile-time-check claims
frozenMap only blocked the Map's own mutators, so
catalog.roles.get("editor").push("*") escalated a role to a full wildcard
past an error string claiming the catalog is frozen after boot; the same
applied to permission/attribute metadata objects and binding arrays.
mergeCatalogs now stores frozen copies of each, so the original declaring
module's objects are never mutated either.

Also corrects two docstrings (codegen.ts, the design doc) that claimed
`wrnexus authz generate`'s output makes a permission typo a type error —
can(), guardPermission(), and decideFor() all take a bare string and nothing
consumes the generated union automatically. Documents what it actually is:
a Permission/Role union to type your own helpers/constants against. Also
adds a README note on the subject.id contract (must be a non-empty string;
owner() compares with Object.is).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:10:24 +05:30
ClintchizandClaude Opus 5 3867e7c183 fix(authz): audit getResource denials; fail closed on a malformed denies shape
guardPermission's getResource catch returned 403 directly, never reaching
decideFor -> decide -> finish, so the audit sink never saw it — an attacker
probing ids that make the resource loader throw got a clean 403 stream
invisible to the audit trail. The audit sink is now stashed on the
per-request RequestAuthz object (authzMiddleware already receives it via
AuthzResolverOptions), and the catch records an "allowed: false" event with
an opaque reason before returning the 403.

Also: the explicit-deny check sat outside decide()'s try/catch, and
deniedBy() guarded on denies.length rather than Array.isArray(denies). A
store returning denies as a bare string let new Set(denies) iterate
characters instead of the permission, so the deny matched nothing and was
silently discarded; a store omitting denies entirely threw straight out of
decide(). Both are now validated and handled inside the try, denying via the
same "Authorization store unavailable" path as any other store failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:10:11 +05:30
ClintchizandClaude Opus 5 a7255fa1bd fix(dev-server): don't clobber a caller-set authz catalog; drop dead RuntimeDeps.authz
createProductionHandlers called setAuthzCatalog unconditionally, so a caller
using client.ts's documented escape hatch (setAuthzCatalog(catalog) before
importing anything that reads it) had that catalog silently wiped to empty
whenever opts.authz was omitted. Now only sets when opts.authz has entries to
contribute, or when nothing has been set yet; a non-empty opts.authz still
always sets and still throws on a genuine conflict.

Also removes RuntimeDeps.authz: nothing read it, and its doc comment
described a consumer that doesn't exist. The real wiring is
getAuthzCatalog()/setAuthzCatalog(), including the HMR hot-update path, which
is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 02:10:00 +05:30
Clintchiz fd5e2b7128 test(authz): end-to-end integration coverage, worked example, and docs
Task 15 of the authz permissions plan: proves db store + cache + catalog +
middleware + audit compose correctly, wires a real (non-dangling) example
into auth-showcase, and documents the declaration/registration/precedence
surface in the package README.
2026-08-05 01:22:13 +05:30
Clintchiz 57097c8204 fix(authz): fix prod boot-order (C1), dev HMR staleness (I2), add prod coverage (I4)
Fix round 2 for Task 14, addressing a critical review finding reproduced on
a real built server.

C1 (critical): the generated production entry set the authz catalog inside
createProductionServer's BODY, but ES modules evaluate every static import
(including app middleware, emitted as a static import) before the importing
module's body runs. Middleware reading getAuthzCatalog() at module scope —
the same eager shape authzMiddleware({ catalog, ... }) itself requires, and
the pattern app/middleware/logger.ts's `export default requestLogger({...})`
already uses — saw an unset catalog and crashed the whole process at import
time, after every other gate (typecheck/lint/tests/a plain `bun run build`)
stayed green.

Fix: packages/cli/src/build.ts now emits a small side-effecting
`.authz-setup.ts` module containing the static imports of every
app/authz/*.ts declaration plus a call to the new
applyAuthzManifestEarly(entries) (packages/dev-server/src/prod.ts), and
imports THAT MODULE FIRST in the generated entry — before pages, api,
realtime, middleware, components, and layouts. applyAuthzManifestEarly is
deliberately silent (no missing-default-export warnings, though a genuine
conflict still throws and fails the boot at import time); createProductionHandlers
keeps its own unconditional merge+set as an idempotent, always-warning second
pass, so an adapter that bypasses the generated entry and calls it directly
still gets a correctly merged, validated catalog, and so the function stays
independently testable.

I3: corrected packages/authz/src/client.ts's WRN-AUTHZ-SETUP message, which
claimed prod always sets the catalog before middleware runs — true again for
the generated entry after the C1 fix, but not for a custom entry that calls
createProductionHandlers directly.

I2: dev HMR editing app/authz/*.ts reloaded the page while the OLD catalog
stayed authoritative (watch.ts classifies any non-CSS change as "server";
hotUpdate had no authz/ branch) — a false security signal, since tightening
or removing a permission looked like it took effect but didn't until a
restart. Added the branch (packages/dev-server/src/index.ts), and gave
loadAppAuthzCatalog (authz-boot.ts) an injectable importer: a raw import()
would have silently no-op'd on the re-import (Bun caches local TS/JS modules
by filesystem path and ignores query strings), so the hot path routes through
loadModule (pipeline.ts) instead, which copies the edited file to a versioned
sibling specifically to defeat that cache.

I4: added direct createProductionHandlers/applyAuthzManifestEarly tests
(packages/dev-server/test/authz-prod.test.ts: conflict throws naming both
files, missing default export warns and skips, empty array yields an empty
catalog, a second call re-validates rather than trusting a stale singleton)
and the regression test that matters most
(packages/cli/test/authz-prod-coldstart.test.ts): a real `runBuild` + a real
`bun dist/server.js` boot, with a middleware module reading
getAuthzCatalog() at module scope, asserting it actually serves a request.

M5: startServer built its own router once, then loadAppAuthzCatalog built a
second one from scratch on every dev boot and every authz/ hot reload.
loadAppAuthzCatalog now accepts either an appDir (still used standalone, e.g.
by the test suite) or an already-built Router, and both call sites in
index.ts now pass the router they already have.

Every fix in this round was verified non-vacuous by sabotaging it and
confirming the corresponding test fails, then reverting.
2026-08-04 23:16:41 +05:30
Clintchiz 226217ecbf feat(authz): reach the merged catalog from boot via a process-wide singleton
Fix round 1 for Task 14 — closes the gap flagged in the last report:
loadAppAuthzCatalog existed but nothing called it.

- packages/authz/src/client.ts (new): setAuthzCatalog/getAuthzCatalog/
  hasAuthzCatalog, mirroring @wrnexus/db's client.ts. App middleware runs
  at module-eval time and needs the catalog then, so ctx cannot carry it;
  getAuthzCatalog() throws a setup error naming the fix, like getDb() does.
  Exported from packages/authz/src/index.ts.
- packages/dev-server/src/index.ts: startServer calls loadAppAuthzCatalog +
  setAuthzCatalog before middleware is resolved (schemasJs precedent),
  and populates the new RuntimeDeps.authz field.
- packages/dev-server/src/runtime.ts: RuntimeDeps gains authz?: AuthzCatalog.
- packages/cli/src/build.ts: emits static imports of each app/authz/*.ts
  file into the generated entry (components/layouts precedent) and passes
  { source, module } pairs through ProdOptions.authz — the catalog holds
  policy functions, so it cannot be JSON-baked like schemasJs.
- packages/dev-server/src/prod.ts: createProductionHandlers merges those
  declarations and calls setAuthzCatalog before the server accepts
  traffic, so a conflict fails the boot instead of surfacing on the first
  request. Runs for every deployment adapter, not only Bun.serve.

The framework never installs authzMiddleware itself; the app still
registers it with its own store.

Verified end-to-end: added a temporary app/authz declaration to
examples/basic-app, ran `bun run build`, inspected the generated entry's
static import + authz array, and booted dist/server.js to confirm the
merge/setAuthzCatalog call succeeds against real bundled code (reverted
before commit).
2026-08-04 22:40:42 +05:30
Clintchiz daea59cf5d feat(dev-server): load the authz catalog at boot
Adds loadAppAuthzCatalog(appDir) to @wrnexus/dev-server: discovers
app/authz/*.ts declarations via buildRouter, imports and merges them
into an AuthzCatalog, returning an empty catalog when the app has no
declarations. A declaration with no default export is skipped with a
warning; a genuine conflict between two declarations throws
WRN-AUTHZ-CONFLICT naming both source files.

Declared the missing @wrnexus/authz workspace dependency in
dev-server's package.json.
2026-08-04 22:03:28 +05:30
Clintchiz bc5437063d fix(cli): declare @wrnexus/authz dependency, exit cleanly on bad authz input
Round-1 review fixes for Task 13:

- packages/cli/package.json was missing @wrnexus/authz, and
  packages/authz/package.json was missing @wrnexus/core despite importing
  its types in index.ts/middleware.ts/advanced.ts. Both only worked
  in-repo because bare "@wrnexus/*" specifiers resolve through the root
  tsconfig.json paths map; a standalone install of @wrnexus/cli or
  @wrnexus/authz would fail at runtime.
- authz.ts's unknown/missing-subcommand and bad --dialect paths now
  console.error + process.exit(1), matching db.ts's convention, instead
  of throwing — index.ts's top-level catch previously printed those as a
  raw stack trace. Added a subprocess-level test that spawns the real CLI
  and asserts stderr has the usage line with no stack frame.
- nextMigrationNumber now extracts the leading-digit run the same way
  db/migrate.ts's nextNumber does, instead of a fixed slice(0, 4) that
  would have undercounted once a migration number passed 9999.
2026-08-04 21:51:38 +05:30
Clintchiz b9098382b3 feat(cli): add wrnexus authz list/generate/init
Introspects the merged authz catalog, emits app/authz/permissions.gen.ts
type unions, and scaffolds the assignment-table migration. init validates
--dialect explicitly (unrecognised values reject rather than silently
falling back to sqlite) and joins authzMigrationSql's up/down statement
lists with terminators instead of interpolating the arrays.

Test scaffolding for dynamically-imported app/authz declarations must
live inside the repo tree (not os.tmpdir()) for the "@wrnexus/*" bare
specifier to resolve via tsconfig paths; .gitignore excludes the scratch
dirs this produces.
2026-08-04 21:34:56 +05:30
ClintchizandClaude Opus 5 e5d0654d2a docs: join the DDL statement lists in the Task 13 init command
authzMigrationSql was changed in Task 11 to return statement arrays rather
than one blob, but Task 13's init still interpolated them straight into the
migration file, which would comma-join two CREATE TABLE statements into one
unparseable line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:26:10 +05:30
ClintchizandClaude Opus 5 3ef353de83 docs: use JSON.stringify for codegen escaping in the Task 12 plan
The plan's union helper hand-rolled escaping for backslash and double quote
only. Role names reach the emitter through the raw mergeCatalogs path, which
does not apply the registry's permission-id regex, so a value containing a
newline was emitted verbatim and the generated file failed to compile with
TS1002 Unterminated string literal.

Caught by the Task 12 implementer actually running tsc over the generated
output rather than eyeballing the string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:20:11 +05:30
Clintchiz 726b8a7d24 feat(authz): generate Permission and Role union types
Emits sorted TS unions from the merged catalog so a typo in
can(ctx, "post:wrtie") is a compile-time error. Uses JSON.stringify
for string-literal escaping (not manual backslash/quote replace) so
role names containing raw newlines still produce valid TypeScript;
role names are not regex-validated like permission ids, so this
matters for the raw mergeCatalogs path.
2026-08-04 21:19:04 +05:30
ClintchizandClaude Opus 5 91e5c6e0c5 fix(authz): guard scopeKey's tenantId type, add deterministic C1/C2 guard
N1: scopeKey guarded the empty-string VALUE but not the TYPE. A
non-string tenantId (null, 0, false, an object) flowed through
un-normalised, and the adapters disagreed about the result - db
rejects null on NOT NULL, memory accepts it as an unreachable row; 0
and false stringify differently and could collide. Now
`typeof tenantId !== "string" || tenantId === ""` is refused with the
same WRN-AUTHZ-SCOPE error. Added a conformance case covering
null/0/false/{}.

N2: nothing failed if grant() were re-wrapped in db.tx, reintroducing
the shared-connection rollback from C1/C2 - timing-based tests can't
reliably prove a transaction is never opened. Added
db-no-transaction.test.ts: a fake Db with a spied driver.transaction
and statement-recording all/exec, driving every PermissionStore method
and asserting zero transaction calls and no "BEGIN" in any recorded
statement. Verified it fails when grant() is temporarily re-wrapped in
db.tx, then restored.

Also documents two things in db.ts as comments only: the UNIQUE
constraints are now load-bearing for ON CONFLICT/ON DUPLICATE KEY
target inference, and MySQL's VALUES(effect) upsert syntax is
deprecated since 8.0.20 (no MySQL server in CI to catch its removal).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:06:11 +05:30
ClintchizandClaude Opus 5 2fbf059c00 docs: type-guard tenantId and add a no-transaction regression guard
Two gaps the Task 11 re-review left open.

scopeKey guarded the empty-string tenantId but not its type, so null, 0,
false or an object flowed through un-normalised and the adapters diverged -
the db rejects on NOT NULL while memory accepts an unreachable row. The whole
premise of the empty-string guard was a caller who controls the tenant id,
and that caller can just as easily hand over a null from a JSON body.

The vacuous concurrency test was removed for good reason, but that left
nothing failing if someone re-wraps grant() in db.tx and reintroduces the
shared-connection rollback. A spy over driver.transaction discriminates that
deterministically, with no timing dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 21:01:26 +05:30
ClintchizandClaude Opus 5 1cc0b97a72 fix(authz): replace vacuous concurrency test, validate effect in memory store
The fix-round-1 test "a concurrent write is not lost to another
method's failure" was vacuous: a single-process Promise.all cannot
reliably land a bare write inside another method's open transaction,
so it passed against both the fixed and the (previously) defective
grant() implementation. The shared-connection rollback hazard it was
meant to catch is real (confirmed separately by forcing the
transaction open before the write), but this specific test could
never reach that state and gave false assurance either way.

Replaced it with "a rejected write leaves unrelated state intact",
which asserts a grant() call with an invalid effect is refused without
disturbing the subject's existing roles/grants, plus a NOTE
documenting that the rollback hazard is now prevented structurally (no
transactions) rather than by a dedicated concurrency test.

memoryPermissionStore.grant() had no effect validation, so it failed
the new test; added a guard mirroring the db adapter's CHECK
constraint so both adapters agree on rejecting anything other than
"allow"/"deny".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:52:18 +05:30
ClintchizandClaude Opus 5 134c5fa4bc docs: replace a vacuous conformance test with an honest one
I added "a concurrent write is not lost to another method's failure" to the
conformance suite to guard the fail-open the Task 11 review demonstrated. The
implementer reported they could not make it fail against the reverted code,
across 600 stress iterations. They were right.

I reproduced the underlying defect directly - forcing the transaction to open
before the bare write gives "revoke resolved without error: true" with the
role still present - so the mechanism is real. But the test cannot reach it:
Promise.all in one process does not reliably land the bare write inside the
open transaction, and grant() never fails on its own. The test passed against
the defective implementation, which is exactly the false assurance this suite
exists to prevent.

Replaced with a property that is actually guaranteed and adapter-agnostic: a
rejected write leaves unrelated state intact. The rollback hazard itself is
prevented structurally, by the store using no transactions, and that is now
stated in a comment rather than pretended to be under test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:48:38 +05:30
ClintchizandClaude Opus 5 205f4e2d4c fix(authz): close fail-open db store defects from review round 1
C1/C2: grant() wrapped its delete+insert in db.tx on a shared,
unserialized sqlite connection, so a concurrent bare write from another
method (e.g. revokeRole) got swept into the open transaction and
discarded on rollback - a revoke could report success while the
privilege survived. Also broke concurrent grants on distinct keys
("cannot start a transaction within a transaction"). Replaced with
single-statement upserts (ON CONFLICT / ON DUPLICATE KEY UPDATE),
atomic without a transaction.

I1: assignRole's check-then-act SELECT lost 19/20 concurrent identical
calls to a UNIQUE violation; switched to ON CONFLICT DO NOTHING.

I2: an unrecognised `effect` value was dropped from both the grant and
deny buckets on read. Added a CHECK constraint and made anything not
literally "allow" count as a deny (fail closed).

I3: ensureAuthzTables defaulted to sqlite instead of the Db's own
dialect. I4: scopeKey now refuses an explicitly empty tenantId rather
than treating it as global (shared with the memory adapter). I5: added
migrations.test.ts asserting the generated DDL per dialect, including
MySQL's binary collation on identity columns. M1: DDL is now a
statement list instead of a blob split on a formatting-dependent
separator. M3: declared @wrnexus/db as a workspace dependency.

Extends the conformance suite with four concurrency/empty-scope tests
(23 total, up from 19) that all three adapters now pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:46:48 +05:30
ClintchizandClaude Opus 5 f19462dff0 docs: fix fail-open concurrency and effect handling in the Task 11 plan
The database store review found two Critical defects and several Important
ones, all reachable in production.

grant() was wrapped in db.tx for atomicity. The sqlite driver runs a bare
BEGIN on one shared connection with no serialization, so an open transaction
swallows any concurrent write from another method and discards it on
rollback. Demonstrated: revokeRole resolved with no error while the role
survived - a security-critical revoke reporting success with the privilege
retained. Concurrent grants also rejected outright with "cannot start a
transaction within a transaction". Replaced with single-statement upserts,
which are atomic without a transaction; assignRole likewise drops its
check-then-act SELECT for ON CONFLICT DO NOTHING, which was rejecting 19 of
20 concurrent identical calls.

effect had no CHECK constraint and assignmentsFor classified by exact
equality, so a mis-cased or corrupted value was dropped from BOTH buckets -
a deny row that silently stopped denying. Added the constraint and made
anything that is not literally "allow" count as a deny.

scopeKey now refuses an explicitly empty tenantId rather than treating it as
global, which otherwise let a caller who controls the tenant id read and
write global assignments.

Also: ensureAuthzTables takes the dialect from db.driver.dialect instead of
defaulting to sqlite; the DDL is a list of statements rather than a blob
split on a formatting-dependent separator; MySQL identity columns get a
binary collation so tenant "T1" cannot match "t1"; and postgres placeholders
are numbered.

Adds four conformance tests for the concurrency and empty-scope cases. The
suite was entirely sequential and structurally could not catch any of this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:33:45 +05:30
Clintchiz 3fa3fce5df feat(authz): add database-backed PermissionStore
Adds dbPermissionStore/ensureAuthzTables/authzMigrationSql, backed by
_wrn_authz_assignment and _wrn_authz_grant tables, plus a ./db subpath
export. Passes the identical 19-test store-conformance suite the memory
adapter passes, including tenant-scope isolation.
2026-08-04 20:22:31 +05:30
ClintchizandClaude Opus 5 218f5e2dd6 chore: add .gitattributes enforcing LF
The repo had none, and core.autocrlf=true is the usual Git-on-Windows
setting, so a clone, checkout, or stash pop silently rewrites every text file
to CRLF. That fails format:check against prettier's endOfLine: lf - it
already turned the gate red once mid-branch, after a stash round-trip
reintroduced CRLF into files that had been committed clean.

Verified: no tracked file currently carries a CR byte at HEAD.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:17:39 +05:30
ClintchizandClaude Opus 5 e136fbc56a fix(router): quietly skip permissions.gen.{ts,js} in authz scan
Task 10 fix round 1: the coordinator's plan doc (41fb82b9) recorded that
generated authz type files should be skipped before the isSafeIslandName
check, but the code change never landed. isSafeIslandName rejects the dot
in the stripped basename "permissions.gen", so every app running Task 12's
codegen would warn on every boot.

Add a quiet skip for *.gen.ts / *.gen.js immediately after the extension
guard, before the name check. Add tests: a .gen.ts file is skipped without
a console.warn (spied), and a .gen.js file is skipped the same way while a
legitimately named .js declaration is still discovered.

Also corrects the scanDir extraExtensions doc comment, which incorrectly
implied app/schemas passes it too (only app/authz does).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 20:09:16 +05:30
ClintchizandClaude Opus 5 41fb82b9e9 docs: skip generated type files in the Task 10 authz scan
The brief asserted permissions.gen.ts would be discovered as an entry named
permissions.gen and filtered by a later task. It is not: isSafeIslandName
rejects the dot in the stripped basename, so it takes the warn-and-skip path
and would print a warning on every boot of any app that ran the codegen,
while Task 14's name-based filter for it was dead code.

The scan now skips *.gen.ts / *.gen.js quietly, before the name check. Also
records the extraExtensions argument the implementer added to scanDir, which
keeps .js out of the route-scanning allow-list where it would otherwise leak
into generated route URLs via fileToRoute.

Caught by the Task 10 implementer testing the claim rather than trusting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:55:33 +05:30
Clintchiz e7743cdbb5 feat(router): discover app/authz declarations
Scan app/authz/<name>.{ts,js} the same way app/schemas is scanned,
exposing Router.authz: ComponentRef[]. Also update the two other
literal Router construction sites (prod runtime, dev-server test
fixture) that now need the new required field.

scanDir gains an optional extraExtensions parameter (default []) so
the authz scan can accept .js files without widening the extension
allow-list used by route scanning (app/pages, app/api, app/realtime),
which would otherwise leak .js into generated route URLs via
fileToRoute.
2026-08-04 19:52:37 +05:30
Clintchiz 703baa1ead fix(authz): strengthen permissionMatches warning, complete export coverage
Move the "don't gate on permissionsFor() with permissionMatches" warning
onto permissionMatches itself so it's visible via autocomplete, not just
on AuthzResolver.permissionsFor. Round out exports.test.ts to cover
scopeKey, safeRecord, and AUTHZ_LOCALS_KEY, closing the gap where
dropping either export from index.ts would not fail the test.
2026-08-04 19:38:07 +05:30
ClintchizandClaude Opus 5 e05ddc7aa5 docs: warn against the permissionMatches + permissionsFor composition
permissionsFor carries a caveat that its Set cannot represent a narrow deny
under a broad grant, so callers must gate with decide(). Now that
permissionMatches is also public, the wrong composition is directly reachable
and looks idiomatic - and the warning lived only on the other half of it.
Adds the pointer to permissionMatches, and covers scopeKey and safeRecord in
the exports test, which the brief omitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:36:24 +05:30
Clintchiz 6f3a53b9ff feat(authz): export registry, store, engine, and middleware surface
Appends the Task 1-8 modules (defineAuthz, catalog merge helpers,
permission stores, audit sinks, resolver engine, and authzMiddleware/
can/guards) to the public @wrnexus/authz surface, and regenerates the
public-api-0.8.json baseline to match.
2026-08-04 19:29:08 +05:30
ClintchizandClaude Opus 5 13859ce7dc docs: add deniedBy to the Task 9 export list
deniedBy was introduced in Task 6's fix round to make wildcard denies work,
but the plan's export block and its exports test were never updated, so
Task 9 would have shipped it module-private.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:25:40 +05:30
Clintchiz e15422ed8d fix(authz): stop authorizeDecision leaking policy names in 403 bodies 2026-08-04 19:17:48 +05:30
Clintchiz 3f1fcd0d2d fix(authz): stop encodeURI from double-encoding a percent-escaped redirectTo
Fix round 3 for Task 7 (N5, minor-to-important): fix round 2's
encodeURI(options.redirectTo) fixed the non-ASCII crash but broke the
most common real use of redirectTo -- a return-path query param that's
already percent-encoded (e.g. /login?next=%2Fdash) -- because encodeURI
also escapes "%", double-encoding it to %252Fdash. Replaced with
headerSafePath(), a codepoint loop that encodes only codepoints above
0x7f (matching isLocalPath's style: no regex, no source escapes) and
leaves "%" alone.

Added tests: an already-percent-encoded target round-trips unchanged;
a non-ASCII target still 303s without throwing and the location is
ASCII-only; a plain ASCII target passes through byte-identical.
2026-08-04 19:10:09 +05:30
ClintchizandClaude Opus 5 798f56734a docs: stop double-encoding redirectTo in the Task 7 plan snippet
The previous fix used encodeURI to keep a non-ASCII redirect target from
throwing inside new Response. But encodeURI also escapes "%", so an
already-percent-encoded target is corrupted: /login?next=%2Fdash becomes
/login?next=%252Fdash, which single-decodes to the literal "%2Fdash" rather
than the intended path. That is the most common real use of redirectTo -
"send them to login, then bounce back".

Replaced with headerSafePath, a codepoint loop that encodes only what cannot
be sent in a Latin-1 header and leaves existing escapes and reserved ASCII
untouched. My prescription, my defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:07:15 +05:30
Clintchiz 77b9e49bf2 fix(authz): fold subject into the memo key, fix symbol/-0 and redirect issues
Fix round 2 for Task 7 (plan amendment 9e3624e5):

- N1 (Important): the memo key carried scope and permission but not the
  subject, so a request that reassigns ctx.user mid-flight (impersonation,
  step-up auth, session revocation, or an authz-before-auth middleware
  ordering mistake) could be served the previous principal's cached
  verdict. subjectId (typeof + String, matching the existing scope/value
  encoding style) is now folded into every memo key.
- N2 (Minor): the primitive-value memo key used String(resource), which
  collapses distinct Symbol("row") values into one slot and maps -0 onto
  0's slot. Added a dedicated bySymbol identity memo (WeakMap-style, but a
  plain Map since symbols aren't valid WeakMap keys pre-registry symbols
  and the memo is request-scoped anyway) and special-cased Object.is(x,-0)
  to render as "-0".
- N3 (Minor): the rejected-redirect console.error interpolated
  redirectTo directly, exactly the value most likely to carry CR/LF in
  that branch. Switched to JSON.stringify(redirectTo) for the log line.
- N4 (Minor): a non-ASCII (but otherwise valid, local) redirectTo passed
  isLocalPath and then threw inside `new Response` building the Location
  header. Wrapped it in encodeURI().

Added 5 regression tests: subject swap re-evaluates, clearing ctx.user
denies, two same-description symbols get separate verdicts, 0 vs -0 get
separate verdicts, non-ASCII redirectTo 303s with an encoded location
instead of throwing. N1 revert-checked: temporarily restored the
two-element (no-subject) key and confirmed both subject-swap tests fail
against it before restoring the fix.
2026-08-04 18:59:56 +05:30
ClintchizandClaude Opus 5 9e3624e584 docs: put the subject in the memo key in the Task 7 plan snippet
The re-review closed all six earlier findings but surfaced the same bug class
one level over: the memo key carried the scope but not the subject, so
reassigning ctx.user mid-request served the previous principal's verdict.
Demonstrated - u1 allowed, then ctx.user = u2 still returned true, and
clearing ctx.user entirely revoked nothing. Triggered by impersonation or
"view as user" middleware, step-up auth, session revocation mid-request, or
simply registering an auth middleware after authzMiddleware.

Also: symbols now memo by identity (String() collapsed two distinct symbols
sharing a description into one slot), -0 stays distinct from 0, the
rejected-redirect log no longer echoes CR/LF verbatim into the log stream,
and a non-ASCII redirect target is encodeURI'd rather than throwing out of
the Response constructor and 500ing on a denial path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:54:30 +05:30
Clintchiz b7f3507b59 fix(authz): close memo cross-authorization and guard hardening gaps
Fix round 1 for Task 7, addressing review findings against the brief's
own memoKey design (now superseded per plan amendment cc8085bc):

- C1: memoKey's String(id) + JSON.stringify-with-catch cross-authorized
  distinct resources whenever their ids stringified the same (numeric
  vs string ids, object-shaped ids) or whenever JSON.stringify threw
  (circular references, BigInt fields, throwing getters all shared one
  "<unserialisable>" bucket, so the first verdict computed for any of
  them became the cached verdict for all of them in that request).
- C2: filterCan inherited the same bypass, returning rows the subject
  could not act on.
- Replaced serialisation-based memoization with identity-based
  memoization: object resources are memoised in a WeakMap keyed by the
  resource reference itself (never serialised), primitives/absent
  resources in a Map keyed by [scope, permission, typeof, String(value)]
  so 7 and "7" can never collide.
- I1: scope is now read from ctx.tenant at decision time (currentScope),
  not captured once at middleware-install time, so a tenant switch
  mid-request is honoured on the next check.
- I2/M1: guardPermission's redirectTo now only fires for non-JSON/API
  requests (replicated wantsJson check, since authz may only import
  core as types) and only for a validated local path (isLocalPath),
  closing an open-redirect and a JSON-caller-follows-303 gap.
- I3: getResource is now wrapped in try/catch; a throw denies with the
  standard opaque 403 body instead of propagating the loader's error
  (e.g. a SQL string) to the client.
- Added cache-control: private, no-store to both the 303 and 403
  responses.

Added 11 regression tests. C1/C2 revert-checked: temporarily restored
the old memoKey design and confirmed the four collision tests fail
against it before restoring the fix.
2026-08-04 18:41:18 +05:30
ClintchizandClaude Opus 5 cc8085bcfa docs: fix memo-key cross-authorization in the Task 7 plan snippet
The middleware's per-request memo keyed resources by String(resource.id) with
an unserialisable fallback that shared one bucket. Six demonstrated cases
cross-authorized: {id:1} vs the primitive 1; {id:7} vs {id:"7"}; object ids;
and every circular / BigInt / throwing-getter row collapsing together so the
first verdict in a request became the verdict for all of them. filterCan
returned 3 of 3 rows where 1 was permitted - it leaked, rather than denied.

Object resources now memo by identity through a WeakMap; primitives key on
JSON-encoded [scope, permission, typeof, value] so 7 and "7" stay distinct
and a tenant id containing the separator cannot collide.

Scope is also read at decision time rather than frozen when the middleware
runs, and is part of the memo key, so switching tenant mid-request no longer
returns the previous tenant's verdict.

guardPermission additionally: denies instead of 500ing when getResource
throws (and no longer leaks the loader's message), skips redirectTo for API
requests using the same rule requireAuth applies, refuses a non-local
redirect target, and sets cache-control: private, no-store.

Adds eleven regression tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:35:05 +05:30
Clintchiz 984c6236d3 feat(authz): add request middleware, can(), and guardPermission
Installs a per-request authz resolver via authzMiddleware and exposes
can()/decideFor()/guardPermission()/filterCan() as free functions (not
Context members, so @wrnexus/core stays free of an authz dependency).
All four route through resolver.decide(), never permissionsFor(), so
resource-scoped policy denials can't be bypassed via the coarse
permission set. Per-request results are memoised keyed on (permission,
resource) to avoid re-hitting the store within a request without
leaking one resource's verdict onto another.
2026-08-04 18:24:40 +05:30
ClintchizandClaude Opus 5 cd82bec414 fix(authz): fix perf, doc, and fail-open gaps found in second review
Re-review of Task 6's fix round 1 (plan amendment d6a2d054) found three
items in that diff plus one adjacent pre-existing issue that C1 made
reachable:

- Important (perf): permissionsFor() rebuilt the deny Set on every
  entry in the granted set (O(grants x denies) allocations on a
  per-request path). Hoisted to build the Set once. Measured
  4000x4000: 665.92ms before, 3.90ms after.
- Important (contract accuracy): permissionsFor() only half-agrees
  with decide() — a narrow deny under a broad grant (e.g. role editor's
  "post:*" plus a deny on "post:delete") can't be represented in a flat
  Set, so the set still contains "post:*" while decide() correctly
  refuses "post:delete". Documented as NOT authoritative on the
  AuthzResolver interface, and pinned with a regression test asserting
  the divergence is deliberate.
- Minor: subject.id === "" was audited as subjectId: "" instead of
  omitted, so consoleAuditSink printed a blank subject= rather than
  subject=anonymous. Reused the same non-empty-string guard as the
  decide() path.
- Important (adjacent, advanced.ts): owner() compared subject[key] to
  resource[key] with Object.is without checking either side was
  present, so two absent ids (Object.is(undefined, undefined) ===
  true) satisfied ownership. Unreachable before this task, but C1 now
  runs bound policies for anonymous/empty subjects, putting this on a
  live path. Fixed to deny whenever either side is undefined or null.

Every fix's regression test was verified by reverting the fix and
confirming the test fails against the pre-fix code before restoring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:15:34 +05:30
ClintchizandClaude Opus 5 d6a2d05407 docs: hoist the deny set and document permissionsFor's limits
Two issues the Task 6 re-review raised against the fix diff.

permissionsFor rebuilt the deny Set inside its loop over granted entries,
making it O(grants x denies) allocations on a per-request path. Measured
632ms at 4000x4000, ~100% of it in repeated Set construction. Hoisted.

permissionsFor also only half-delivers on "the obvious composition agrees
with decide()". A narrow deny beneath a broad grant is not representable in
a Set of strings - the set keeps post:* while decide() correctly refuses
post:delete - so callers that match against the set would offer actions the
server rejects. Documented the limit on the interface and pointed callers at
decide()/can()/filterCan() for per-action gating.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 18:09:34 +05:30
ClintchizandClaude Opus 5 ae37c9b57a fix(authz): close fail-open engine gaps found in review
Coordinator review of Task 6's resolution engine (plan amendment
86b3dc1e) found two critical and four important defects, all inherited
from the brief's original engine snippet:

- C1: anonymous callers on a public permission returned allow before
  running bound policies, so the least-trusted caller got the weakest
  evaluation. Policies now run for anonymous subjects too.
- C2: the policy verdict check was a truthiness test (`!verdict.allowed`),
  so a policy returning `{allowed: "yes"}` granted access. Now requires
  `verdict?.allowed === true` exactly, and no longer spreads the raw
  verdict into the decision (which leaked arbitrary policy fields).
- I1: a binding naming a policy the catalog doesn't have was silently
  `continue`d, granting whatever the policy was meant to guard. Now
  denies with "Policy unavailable".
- I3: denies were checked by exact string equality, so a wildcard deny
  (e.g. "post:*") was accepted and silently did nothing. Denies now go
  through the same depth-aware wildcard matching as grants, via the new
  exported `deniedBy()`.
- I2: `permissionsFor` now subtracts denied entries so it agrees with
  `decide()` — needed for Task 7's UI gating to compose correctly.
- I4: non-string/empty `subject.id` (0, "", 123, {}) no longer silently
  falls back to anonymous; it denies with "Invalid subject". `subject:
  null` (no subject at all) remains genuinely anonymous.

Added six regression tests, each verified by reverting its fix and
confirming the test fails against the old code before restoring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:58:28 +05:30
ClintchizandClaude Opus 5 86b3dc1e6a docs: close two auth bypasses and four fail-open paths in the Task 6 engine snippet
The plan's engine had a genuine authorization bypass and several fail-open
branches. Task 7 builds can() on this, so the source of truth is fixed before
that lands.

CRITICAL - anonymous callers bypassed every bound policy on a public:true
permission: the anonymous branch returned allow before the policy loop. A
permission marked "public, but not when embargoed" was fully open to
unauthenticated traffic, and the least-trusted caller got the weakest
evaluation. Policies now run on the anonymous path too; public relaxes the
identity requirement, never the policy requirement.

CRITICAL - the policy verdict check was truthiness-based, not an identity
check, so a policy returning {allowed: "yes"} or {allowed: 1} granted access.
It now compares against true.

A binding naming a policy the catalog lacks was skipped, granting whatever
the policy guarded; it now denies. Falsy and non-string subject ids fell
through to the anonymous path - {id: 0} became anonymous and {id: 123} reached
the store as a lookup key; only a non-empty string now identifies a subject.

Two design forks, ruled by the human: denies honour wildcards, so denying
"post:*" blocks post:delete instead of being accepted and doing nothing; and
permissionsFor subtracts denies, so composing it with permissionMatches
agrees with decide() rather than silently losing deny precedence.

Adds deniedBy() and six regression tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:52:32 +05:30
ClintchizandClaude Opus 5 c499f136fd docs: fix self-contradictory audit test in the Task 6 plan snippet
The 'denials are audited' test assigned role editor, which holds post:*, so
decide(post:delete) was legitimately an ALLOW under the wildcard rule the
same task specifies. The test then asserted one audited denial and got zero.
Switched to moderator (post:comment:*), which genuinely lacks post:delete.

Caught by the Task 6 implementer running the transcribed test against the
transcribed implementation. Plan-origin defect, fixed under standing
authority.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:41:11 +05:30
Clintchiz 6d8b6daba9 feat(authz): add resolution engine with deny-wins precedence and fail-closed errors 2026-08-04 17:40:19 +05:30
Clintchiz d7509421c7 fix(authz): widen logSafe to strip NEL and Unicode line separators
U+0085 (NEL), U+2028 (LINE SEPARATOR), and U+2029 (PARAGRAPH SEPARATOR)
are treated as line terminators by some log shippers and by JS's own
lexical grammar (and are not escaped by JSON.stringify by default), so
they could still be used to forge audit log entries even after the
initial C0/DEL fix. logSafe now strips all five categories.
2026-08-04 17:29:46 +05:30
ClintchizandClaude Opus 5 d609a41222 docs: widen logSafe to Unicode line separators in the Task 5 plan snippet
The re-review confirmed the log-injection fix works for C0 and DEL, but
U+0085 (NEL) and U+2028/U+2029 pass through. Those are line terminators to
some log shippers and to JavaScript's own lexical grammar, so they can still
split a record downstream.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:26:02 +05:30
Clintchiz e710756baf fix(authz): sanitize control characters in console audit sink
Prevents audit log injection: subjectId, tenantId, and reason trace back
to request input, so an unsanitized newline could forge a second,
fully-formed audit line indistinguishable from a real entry. Adds
logSafe() to strip control characters before interpolation and logs the
previously-missing policy field.
2026-08-04 17:21:09 +05:30
ClintchizandClaude Opus 5 ba83038d8d docs: fix audit-log injection in the Task 5 plan snippet
consoleAuditSink interpolated subjectId, tenantId and reason straight into
the log line. A newline in any of them forges a second entry that reads as a
genuine audit record - the reviewer produced a fake
'[wrnexus:authz] allow admin:everything subject=root' line. Those values
trace back to request input.

Interpolated fields now go through logSafe(), which replaces control
characters. Adds the missing coverage the review flagged: consoleAuditSink
injection, malformed-sink handling, and memoryAuditSink.clear().

Plan-origin defect, fixed under standing authority to amend the plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:19:05 +05:30
Clintchiz f033197850 feat(authz): add pluggable authorization audit sink 2026-08-04 17:10:03 +05:30
Clintchiz dc0771308a fix(authz): eliminate cache-key collision in cachedPermissionStore
The scope-prefix concatenation cacheKey used a bare U+FFFD separator with
no escaping, so an adversarial subject/tenant id containing that character
could collide with a different subject/tenant pair and leak cached roles
across tenants. Switch to JSON.stringify([scopeKey, subjectId]) for an
unambiguous key.

Also replace the untested key.endsWith() substring sweep used to
invalidate a subject across all tenants on a global write with an
explicit bySubject index, and add test coverage for both the collision
and the cross-tenant invalidation sweep.
2026-08-04 17:04:19 +05:30
ClintchizandClaude Opus 5 83f2951035 docs: fix cache-key collision in the Task 4 plan snippet
The plan's cachedPermissionStore used scopeKey + U+FFFD + subjectId as a
cache key with no escaping, so ('a', 'b<sep>c') and ('a<sep>b', 'c') collide
and one subject is served another's permissions. Subject and tenant ids are
unconstrained strings, so nothing prevented it.

Key is now JSON-encoded, and the global-write sweep tracks keys per subject
instead of substring-matching. Adds the two regression tests that were
missing: cross-tenant invalidation on a global write, and key collision.

Ruled by the human as plan-mandated; source of truth amended so a re-run of
the plan does not reintroduce the defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 17:00:35 +05:30
Clintchiz 4362d49770 feat(authz): add caching decorator for PermissionStore 2026-08-04 16:47:50 +05:30
Clintchiz a01b7bc99e fix(authz): cover grant/deny scope isolation and revoke scope-isolation in conformance suite 2026-08-04 16:43:08 +05:30
ClintchizandClaude Opus 5 9b6b970cae chore: exclude the SDD scratch workspace from prettier
.superpowers/ holds git-ignored controller artifacts (briefs, reports,
review packages). Prettier still walked it, so format:check — and with it
check:production — failed on scratch markdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:38:24 +05:30
Clintchiz 1849213ce4 feat(authz): add PermissionStore contract with memory adapter and conformance suite 2026-08-04 16:36:57 +05:30
Clintchiz d694dda320 feat(authz): merge declaration modules into a frozen catalog 2026-08-04 16:30:41 +05:30
Clintchiz 212fdaa5b5 feat(authz): add defineAuthz declaration registry 2026-08-04 16:26:17 +05:30
ClintchizandClaude Opus 5 0ac648bc26 docs: resolve two pre-flight conflicts in the authz plan
- Global Constraints said the change was additive while Task 8 changed
  authorizeDecision's 403 body. Ruled: the security fix governs; the
  constraint now names it as the one approved exception.
- Task 6 defined permissionsFor and then re-implemented it inline in
  decide. Both now call a single loadEffective helper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:24:07 +05:30
ClintchizandClaude Opus 5 10da210b0a docs: implementation plan for the authz permissions system
Fifteen TDD tasks covering phases 1-3 of the approved design: registry,
catalog merge, PermissionStore with a shared conformance suite, caching
decorator, audit sink, resolution engine, request middleware and guards,
router discovery, database adapter, codegen, and the wrnexus authz CLI.

Phases 4 (.wrn view can()) and 5 (admin UI) are documented as deferred with
the reason each needs its own design pass.

Also folds in the authorizeDecision disclosure fix as Task 8, since the new
guards share its 403 shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:10:37 +05:30
ClintchizandClaude Opus 5 b209936f86 docs: design for the authz permissions system
Separates declaration (what permissions, roles, policies and attributes exist)
from assignment (who holds what), building on the decision primitives already
in advanced.ts rather than replacing them.

Covers the registry and app/authz discovery, the PermissionStore interface
with memory and db adapters, tenant-scoped assignments meeting the existing
TenantMembership, deny-wins precedence, fail-closed behaviour, the audit sink,
codegen and CLI introspection, and the seam for propagating subject context to
the inter-app communication system.

Records two decisions worth keeping: cross-app sharing needs no runtime
catalog distribution (declarations are static code in the shared package;
only assignments are shared, via the database), and can() stays off Context
to avoid a core -> authz dependency cycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:57:22 +05:30
ClintchizandClaude Opus 5 c64434a131 fix(security): close SSRF, credential-leak, and auth bypass findings in 0.8.4
Audit of 0.8.4 found the repo's own gates green, so these came from manual
review; each is covered by a new regression test.

security/fetch.ts
- safeFetch re-attached Authorization/Cookie on a same-origin redirect that
  followed a cross-origin hop (a -> b -> b), handing credentials to the second
  host. Compare against the origin the caller trusted, not the previous hop.
- The private-network guard resolved the host, approved it, then let fetch
  resolve again, so a low-TTL record could answer public for the check and
  private for the connection. Pin the connection to the validated address,
  preserving Host and TLS serverName. Opt out with pinDns: false.
- 0:0:0:0:0:ffff:127.0.0.1, ::ffff:7f00:1 and fec0::1 were not treated as
  private. Add uncompressed IPv4-mapped forms, site-local IPv6, 198.18/15
  and 192.0.0/24.

security/url.ts
- sanitizeUrl returned "//evil.com" verbatim via the relative-path fast path,
  bypassing the host checks it had just run; in an href that navigates
  cross-origin. Resolve protocol-relative input instead.

dev-server/gateway.ts
- Malformed base64 in an Authorization header threw out of checkAuth on an
  unauthenticated path. Fail closed.
- split(":", 2) truncated passwords at the first colon, so a password
  containing ":" could never authenticate.
- The credential compare short-circuited on length mismatch, leaking length
  by timing. Extracted as verifyBasicAuth so it is testable.

authz/index.ts
- Namespace wildcards only matched the first segment, so "post:comment:*"
  did not grant "post:comment:delete". Match at every depth.

uploader/operations.ts
- Validate transcoder dimensions and bitrate rather than trusting the declared
  type, and reject ".." path segments.

package.json
- The brace-expansion override pinned 5.0.8, which is inside the advisory
  range >=4.0.0 <5.0.9. Bump to 5.0.9; bun audit is now clean.

Verified: check:production passes (typecheck, lint, 1033 tests, format,
ASVS, public-API baseline, editor checks).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 15:57:22 +05:30
Clintchiz 72e4d3eceb release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 12m19s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-04 12:19:09 +05:30
Clintchiz 4cebacadfe release: WRNexusJS 0.8.3
Quality / quality (ubuntu-latest) (push) Failing after 12m9s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 19:47:30 +05:30
Clintchiz e8f630f12d fix: format generated docs before release verification
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 02:32:11 +05:30
Clintchiz 4550a11460 release: WRNexusJS 0.8.2
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 02:14:54 +05:30
Clintchiz 3c6b659f36 release: WRNexusJS 0.8.1
Quality / quality (ubuntu-latest) (push) Failing after 13m28s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 01:34:56 +05:30
Clintchiz 1a1d2e9d08 perf: accelerate production request hot paths
Quality / quality (ubuntu-latest) (push) Failing after 12m23s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 01:08:43 +05:30
Clintchiz fed1d5d3f4 perf: omit unused UI CSS and minify final bundles
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 01:03:33 +05:30
Clintchiz b61020babd fix: update PWA workers without reloading pages
Quality / quality (ubuntu-latest) (push) Failing after 12m52s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 00:54:29 +05:30
Clintchiz 379f80cbd0 fix: force PWA worker updates
Quality / quality (ubuntu-latest) (push) Failing after 12m26s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 00:46:52 +05:30
Clintchiz 649e3d9127 fix: constrain PWA caching and extend font CSP
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 00:38:23 +05:30
Clintchiz b3e9b99e13 fix: harden generated identifiers and types
Quality / quality (ubuntu-latest) (push) Failing after 12m50s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-03 00:02:37 +05:30
Clintchiz 586a6db8ff release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
2026-08-02 23:18:51 +05:30
907 changed files with 286874 additions and 25613 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "component-showcase",
"runtimeExecutable": "bun",
"runtimeArgs": ["run", "--cwd", "examples/component-showcase", "dev"],
"port": 3000
}
]
}
+16
View File
@@ -0,0 +1,16 @@
# Enforce LF in the working tree regardless of a contributor's core.autocrlf.
# Without this, Git on Windows smudges every text file to CRLF on clone, stash
# pop, or checkout, which fails `bun run format:check` (prettier endOfLine: lf).
* text=auto eol=lf
# Binary assets Git must not touch.
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.webp binary
*.ico binary
*.pdf binary
*.woff binary
*.woff2 binary
*.db binary
+50
View File
@@ -0,0 +1,50 @@
name: Quality
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
quality:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.14
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: editors/vscode/package-lock.json
- name: Install framework dependencies
run: bun install --frozen-lockfile
- name: Install editor dependencies
run: npm ci --prefix editors/vscode
- name: Typecheck
run: bun run typecheck
- name: Lint
run: bun run lint
- name: Formatting
run: bun run format:check
- name: Package and application tests
run: bun run test:all
- name: Package contracts
run: bun run check:public-api && bun run check:ui-visual && bun run audit:packages && bun run test:package-kits && bun run validate:staging
- name: Stage and test publishable packages
if: matrix.os == 'ubuntu-latest'
run: bun run stage:packages && bun run test:staged-consumers
- name: Framework validation
run: bun run validate:0.8
- name: Dependency audit
run: bun audit
- name: Editor dependency audit
run: npm audit --prefix editors/vscode --audit-level=high
+5
View File
@@ -19,3 +19,8 @@ bun.lockb
# Local focused typecheck helpers must never enter the repository. # Local focused typecheck helpers must never enter the repository.
focus-shims.d.ts focus-shims.d.ts
tsconfig.focus.json tsconfig.focus.json
# Scratch dirs for tests that must dynamically import scaffolded files using
# "@wrnexus/*" bare specifiers (resolved via the root tsconfig.json `paths`,
# which requires the scaffold to live inside the repo tree).
**/test/.tmp-*/
+3
View File
@@ -0,0 +1,3 @@
@wrnexus:registry=https://registry.npmjs.org/
audit=true
fund=false
+8
View File
@@ -10,9 +10,14 @@ 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
# Bundled .wire compiler for the VS Code extension (generated) # Bundled .wire compiler for the VS Code extension (generated)
editors/vscode/src/compiler.cjs editors/vscode/src/compiler.cjs
editors/vscode/src/language-server.cjs
editors/vscode/src/extension.bundle.cjs
docs/public-api-0.8.json
docs/ui-visual-contract-0.8.json
*.svg *.svg
**/.vscodeignore **/.vscodeignore
@@ -21,3 +26,6 @@ focus-shims.d.ts
**/focus-shims.d.ts **/focus-shims.d.ts
tsconfig.focus.json tsconfig.focus.json
**/tsconfig.focus.json **/tsconfig.focus.json
# SDD scratch workspace (git-ignored controller artifacts)
.superpowers/
+32
View File
@@ -1,5 +1,9 @@
# @wrnexus/ai # @wrnexus/ai
Provider-neutral AI orchestration for OpenAI, Anthropic, Google and local OpenAI-compatible models,
with streaming, structured output, tools, embeddings, vector search/RAG, conversation persistence,
templates, guardrails, usage events, fallback, rate limits and evaluation reports.
> A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code. > A tiny, zero-dependency Claude (Anthropic) client for WrNexus apps — generate and stream text with Claude from any server-side code.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
@@ -120,6 +124,34 @@ try {
} }
``` ```
### Multi-provider client
`createAIClient` adds named-provider selection and fallback, capability discovery,
validated JSON output, validated tool execution, abort-aware exponential retries,
and per-provider circuit breakers. Attempt events intentionally contain metadata
only: prompts, credentials, and raw model responses are never passed to telemetry.
```ts
import { anthropicProvider, createAIClient } from "@wrnexus/ai";
const ai = createAIClient({
providers: [anthropicProvider()],
retry: { attempts: 3, baseDelayMs: 100, maxDelayMs: 2_000 },
circuitBreaker: { failureThreshold: 5, resetAfterMs: 30_000 },
});
const result = await ai.generateObject<{ title: string }>("Return a JSON title", {
validate: (value): value is { title: string } =>
typeof value === "object" && value !== null && "title" in value,
});
```
Providers can return normalized `usage` (`inputTokens`, `outputTokens`,
`totalTokens`, and `costUsd`) and `toolCalls`. Use `executeTools` with a named,
validated tool registry; unknown tools and invalid arguments are rejected before
application code runs. `deterministicAIProvider` supplies ordered or computed
offline responses for tests and examples without API keys or network calls.
## Usage ## Usage
### Return generated JSON from an API route ### Return generated JSON from an API route
+23 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ai"
},
"homepage": "https://wrnexusjs.dev/packages/ai",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ai"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,9 +34,14 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
},
"./platform": {
"types": "./dist/platform.d.ts",
"import": "./dist/platform.js"
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+153
View File
@@ -149,3 +149,156 @@ app.put(
- **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported. - **Bun-only** — like the rest of WrNexus, this package targets the Bun runtime; Node is not supported.
- Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`. - Works with [`@wrnexus/core`](../core) — the guards return `Middleware` and read the subject from `ctx.user` on the request `Context`. Both types are imported from `@wrnexus/core`.
- Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise<boolean>` (e.g. for a database ownership check). - Policy combinators (`any`, `all`) and `authorize` are async-aware, so policies may return a `Promise<boolean>` (e.g. for a database ownership check).
## Declaring permissions
The RBAC/PBAC/ABAC surface above is the low-level toolkit. On top of it sits a
declarative **registry + catalog + store + engine**: permissions, roles, and
policies are declared once in code, merged into a frozen catalog at boot, and
resolved per-request against a pluggable `PermissionStore` that holds who has
what.
Put declarations in `app/authz/<name>.ts`; they are discovered automatically
and merged (conflicting declarations of the same permission/role/policy across
files fail the boot loudly, naming both source files).
```ts
import { defineAuthz, owner } from "@wrnexus/authz";
export default defineAuthz({
permissions: {
"post:read": { title: "View posts", public: true },
"post:delete": { title: "Delete posts", risk: "high" },
},
// "post:*" is a namespace wildcard grant, valid inside a role's list — it is
// not itself a registered permission, so it can only ever grant permissions
// that ARE declared above (e.g. "post:read", "post:delete").
roles: { editor: ["post:*"], admin: ["role:editor"] },
policies: { ownsPost: owner("id", "authorId") },
bindings: { "post:delete": ["ownsPost"] },
});
```
`public: true` means anonymous callers may hold the permission — but any
policy bound to it still runs, and can still veto the anonymous caller (e.g. a
`notBanned` policy on a public `post:preview` permission).
## Checking permissions
Register `authzMiddleware` once, in `app/middleware/`, with the merged
catalog and a `PermissionStore`. Like every other `app/middleware/*.ts` file,
the registration is an eager, module-scope call — the same shape as
`authzMiddleware({ catalog, store })` requires — so it must run after the
catalog has been populated. Both the dev server and `wrnexus build`'s
generated production entry guarantee `getAuthzCatalog()` is populated before
any app middleware module evaluates. Name the file so it sorts after whatever
middleware sets `ctx.user` (middleware runs in alphabetical filename order —
`authz.ts` after `auth.ts`, for instance).
```ts
// app/middleware/authz.ts
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
import { dbPermissionStore } from "@wrnexus/authz/db";
import { getDb } from "@wrnexus/db";
export default authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) });
```
> **`subject.id` must be a non-empty string.** The engine denies (and logs to
> stderr) whenever `ctx.user.id` is present but not a non-empty string — this
> includes the common case of an integer primary key. Coerce it before it
> reaches `ctx.user`, e.g. `user.id = String(row.id)`, or every request for
> that user denies with "Invalid subject" instead of resolving normally.
> `owner()` (the built-in ownership policy) compares subject and resource ids
> with `Object.is`, so both sides must be the same type too — `owner()` on a
> numeric `resource.authorId` against a stringified `subject.id` never
> matches even when they represent "the same" id.
There is no per-route `middleware` export — `app/middleware/*.ts` is the only
place middleware is registered. To gate part of the app, branch on the
request the same way any other conditional middleware does (compare
`app/middleware/captcha-login.ts` in the auth showcase, which branches on
method + path the same way):
```ts
// app/middleware/protect-posts.ts
import type { Context, Next } from "@wrnexus/core";
import { guardPermission } from "@wrnexus/authz";
const guardPostWrite = guardPermission("post:write");
export default function protectPosts(ctx: Context, next: Next) {
return ctx.url.pathname.startsWith("/api/posts") && ctx.req.method !== "GET"
? guardPostWrite(ctx, next)
: next();
}
```
Or check inline inside a route handler with the free function `can()`:
```ts
// app/api/posts/[id].ts
import type { Context } from "@wrnexus/core";
import { can } from "@wrnexus/authz";
export const DELETE = async (ctx: Context) => {
const post = { id: "1", authorId: "alice" }; // load your own resource here
if (!(await can(ctx, "post:delete", post))) {
return Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
}
return Response.json({ ok: true });
};
```
`can()` is a free function taking `ctx`, not `ctx.can``@wrnexus/core` must
not depend on `@wrnexus/authz`, so the per-request resolver lives in
`ctx.locals` instead, reached through `can()` / `decideFor()` /
`guardPermission()` / `filterCan()`. Calling any of them before
`authzMiddleware` has run for that request throws a `WRN-AUTHZ-SETUP` error
naming the missing registration, rather than silently denying.
See `examples/auth-showcase/app/authz/showcase.ts` and
`examples/auth-showcase/app/middleware/authz.ts` for a complete, runnable
version of this wiring.
## Precedence
1. An explicit deny wins over everything, including `*` — and honours the
same namespace-wildcard matching as grants (denying `post:*` blocks
`post:comment:delete`, not just `post:*` itself).
2. A bound policy can veto a permission a role grants, and runs even for a
`public: true` permission — including for an anonymous caller.
3. Otherwise the permission must be held via a role or an explicit grant.
4. Default deny.
Every failure — an unknown permission (outside strict/dev mode), a store
outage, a thrown policy — denies rather than throwing through to the caller.
`permissionsFor()` (on the resolver returned by `createAuthzResolver`) is a
coarse hint for hiding UI (e.g. a menu section), **never authoritative**. A
`Set<string>` cannot represent "granted `post:*` except `post:delete`", so a
narrow deny beneath a broad grant is invisible to it — the set still contains
`post:*` while `can()` / `decide()` correctly refuse `post:delete`. Gate real
actions with `can()`, `decideFor()`, or `filterCan()`; never by matching
against `permissionsFor()`'s result.
## CLI
```bash
wrnexus authz list # every registered permission, role, and policy
wrnexus authz generate # app/authz/permissions.gen.ts type unions
wrnexus authz init # scaffold the assignment-table migration
```
`wrnexus authz generate`'s output is a plain `Permission | Role` string-literal
union — `can()`, `guardPermission()`, and `decideFor()` all take a bare
`string` and nothing reads this file automatically, so import it to type your
own helpers/constants against the registered catalog, e.g.:
```ts
import type { Permission } from "app/authz/permissions.gen.ts";
function guard(permission: Permission) {
return guardPermission(permission);
}
```
+27 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/authz"
},
"homepage": "https://wrnexusjs.dev/packages/authz",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"authz"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,9 +34,18 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
},
"./db": {
"types": "./dist/db.d.ts",
"import": "./dist/db.js"
} }
}, },
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/db": "^0.8.5"
},
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+54 -2
View File
@@ -1,5 +1,18 @@
# @wrnexus/cli # @wrnexus/cli
Production parity commands:
```bash
wrnexus build .
wrnexus preview . --port=3000
wrnexus dev . --production-runtime
```
`preview` refuses to start without `dist/server.js` and executes that exact
artifact with the production profile. Production-runtime development rebuilds
the same minified artifact after app, public, or configuration changes and
keeps the last good server running when a rebuild fails.
> The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps. > The `wrnexus` command-line tool that scaffolds, runs, builds, tests, and manages WrNexus apps.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
@@ -26,6 +39,30 @@ bunx wrnexus dev
## Commands ## Commands
### Local production services
`wrnexus dev . --services` starts the application and the bounded local database,
cache, mail, SMS, webhook, storage, queue, cron, authentication and metrics simulator.
It generates a localhost/`*.localhost` development certificate under
`.wrnexus/certificates/` and serves both the application and service console over HTTPS.
Trust that certificate locally to remove the browser warning. Use `--services-http` only
when an external development proxy already terminates TLS.
### Exact production runtime with live updates
`wrnexus dev . --production-runtime` rebuilds and executes `dist/server.js` with
production resolution, serialization, caching, headers and assets. The supervisor keeps
the last good process when a build fails. On a successful rebuild the opt-in production
HMR socket reconnects, requests the new document and morphs it into the browser; ordinary
`wrnexus preview` and deployed production servers never include that client.
### API platform
`wrnexus api generate [app-dir]` (or `api docs`) derives operations from file routes and
emits `generated/api/openapi.json`, safe static documentation, Postman collection, curl examples,
and TypeScript, JavaScript, Java, Go and Python SDKs. Generate one client with
`wrnexus sdk generate <language> [app-dir]`.
Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=<name>` (see [Profiles](#profiles)). Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that read config or `.env` also accept `--profile=<name>` (see [Profiles](#profiles)).
| Command | Purpose | | Command | Purpose |
@@ -46,10 +83,17 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus db <cmd>` | Database migrations and tooling (see [db](#wrnexus-db)). | | `wrnexus db <cmd>` | Database migrations and tooling (see [db](#wrnexus-db)). |
| `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). | | `wrnexus test [app-dir] [--watch]` | Run the app's tests via `bun test` (defaults to the `test` profile). |
| `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. | | `wrnexus profiles [app-dir]` | List config profiles and their `.env` files, marking the active one. |
| `wrnexus compatibility check` | Check whether behavior defaults are explicitly pinned and current. |
| `wrnexus compatibility explain` | Explain configured, effective, and current compatibility behavior. |
| `wrnexus compatibility upgrade` | Back up config and explicitly opt into reviewed current behavior. |
| `wrnexus help` | Print usage. | | `wrnexus help` | Print usage. |
`wrnexus g` is an alias for `wrnexus generate`. `wrnexus g` is an alias for `wrnexus generate`.
Compatibility upgrades never happen implicitly. New applications pin
`compatibilityDate` and `frameworkBehaviour`; existing applications use
`wrnexus compatibility explain` before the backed-up, idempotent upgrade command.
### `wrnexus dev` ### `wrnexus dev`
Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`). Supervises a child dev-server process (from `@wrnexus/dev-server`). The child owns file watching and HMR: CSS and client-island edits update the live page over a WebSocket with no restart; when a server module changes, the child exits with a restart code and the supervisor respawns it (the browser reconnects and morphs in the new HTML). On startup it regenerates typed DB queries and typed routes (best effort). Use `--port=` to change the port (default `3000`).
@@ -76,7 +120,9 @@ bun dist/server.js # PORT env var optional
### `wrnexus create` ### `wrnexus create`
Scaffolds a new app from an inline (dependency-free) template — `package.json`, `.gitignore`, config, and starter `app/` files. Use `npm run dev` during development, `npm run build && npm start` for production, or `npm run production` to build and start in one command. The generated production server currently requires Bun even when npm is used to manage packages and scripts. Scaffolds a complete v0.8 app from an inline template. The generated project includes strict TypeScript, ESLint and Prettier, editor recommendations, environment templates, database migrations, locales, schemas, tests, API/middleware/realtime examples, Tailwind and Iconify, PWA/mobile defaults, and the framework package kits. Its `wrnexus.config.ts` documents the current imports, types, stores, performance, observability, tenancy, build, navigation, theme, i18n, database, storage, realtime, security, and profile configuration.
Use `bun run dev` during development, `bun run check` for the complete typecheck/lint/test/format gate, `bun run build && bun run start` for production, or `bun run production` to build and start in one command.
### `wrnexus update` ### `wrnexus update`
@@ -162,7 +208,7 @@ wrnexus db status --db=analytics
### `wrnexus workspace` and `wrnexus gateway` ### `wrnexus workspace` and `wrnexus gateway`
`workspace <name>` scaffolds a monorepo: several WrNexus apps under `apps/*` and shared libraries under `packages/*`, plus a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). `workspace <name>` scaffolds a monorepo: complete v0.8 apps under `apps/*`, shared libraries under `packages/*`, root TypeScript/lint/format/editor/environment tooling, and a `wrnexus.workspace.ts` that maps each app to the domains it serves. `gateway` runs every app behind one port and routes by `Host` header, with optional per-app auth and gateway-wide security (trusted hosts, rate limit, security headers, access log). Newly added workspace apps use the same current scaffold.
```bash ```bash
wrnexus workspace acme wrnexus workspace acme
@@ -241,6 +287,12 @@ wrnexus update --latest
wrnexus doctor wrnexus doctor
``` ```
Use `wrnexus doctor --fix [app-dir]` to apply conservative repairs before the
health check: create missing `app/pages` and a default config, align skewed
`@wrnexus/*` dependency ranges, record the current migration marker, and format
only syntax-valid `.wrn` files. Invalid JSON or WRN sources are reported/skipped
instead of overwritten; repeat runs are idempotent.
## Profiles ## Profiles
Pass `--profile=<name>` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.<profile>`, `.env.<profile>.local`) into `process.env`. Pass `--profile=<name>` to `dev`, `build`, `db` (or set `WRNEXUS_PROFILE`) to select a config profile. The CLI publishes `WRNEXUS_PROFILE` so config loaders and the dev child pick it up, and loads that profile's `.env` cascade (`.env`, `.env.local`, `.env.<profile>`, `.env.<profile>.local`) into `process.env`.
+37 -14
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.", "description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/cli"
},
"homepage": "https://wrnexusjs.dev/packages/cli",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"cli"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -28,20 +44,27 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0", "@wrnexus/core": "^0.8.5",
"@wrnexus/router": "^0.7.0", "@wrnexus/router": "^0.8.5",
"@wrnexus/csr": "^0.7.0", "@wrnexus/csr": "^0.8.5",
"@wrnexus/compiler": "^0.7.0", "@wrnexus/compiler": "^0.8.5",
"@wrnexus/styles": "^0.7.0", "@wrnexus/styles": "^0.8.5",
"@wrnexus/dev-server": "^0.7.0", "@wrnexus/dev-server": "^0.8.5",
"@wrnexus/ui": "^0.7.0", "@wrnexus/ui": "^0.8.5",
"@wrnexus/validation": "^0.7.0", "@wrnexus/validation": "^0.8.5",
"@wrnexus/i18n": "^0.7.0", "@wrnexus/i18n": "^0.8.5",
"@wrnexus/db": "^0.7.0", "@wrnexus/mcp": "^0.8.5",
"@wrnexus/plugin": "^0.7.0", "@wrnexus/playground": "^0.8.5",
"@wrnexus/syntax": "^0.7.0" "@wrnexus/db": "^0.8.5",
"@wrnexus/authz": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/syntax": "^0.8.5",
"@wrnexus/typecheck": "^0.8.5",
"@wrnexus/security": "^0.8.5",
"selfsigned": "^5.5.0"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+35
View File
@@ -1,9 +1,44 @@
# @wrnexus/compiler # @wrnexus/compiler
## Partial-static rendering
Pages can select `render = "partial-static"` and divide their view with `<Static>` and
`<Dynamic>` boundaries. The compiler emits a build-only shell renderer that never evaluates
dynamic-boundary children. `wrnexus build` expands static component mounts into
`dist/partial-shells.json`, records byte/region evidence in `build-report.json`, and embeds
the shell in the production route manifest. At request time the production runtime retains
request-aware layouts, locale/theme metadata and security nonces while streaming dynamic
regions into stable placeholders.
> Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript. > Compiler for the `.wrn` language — tokenizes, parses, and lowers `.wrn` page and component files to TypeScript.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
Production adapters use `analyzeRuntimeImports` before bundling. Edge, worker,
service-worker, and browser targets reject Node filesystem, TCP, and process
modules with `WRN-RUNTIME-CAPABILITY`. Package manifests can declare supported
`wrnexus.runtimes` and required `wrnexus.requires` capabilities; discovery fails
when the selected deployment cannot satisfy them.
## Server actions
```wrn
action createUser using CreateUserSchema {
const user = await users.create(input)
invalidate("users")
return user
}
view {
<form @submit="createUser">...</form>
}
```
The compiler produces a schema-aware server registry, a fully inferred action
client, and progressively enhanced form metadata. The shared runtime performs
validation, authentication/permission checks, CSRF verification, serialization,
invalidation reporting, and browser lifecycle events.
## Overview ## Overview
`@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page. `@wrnexus/compiler` turns `.wrn` source into TypeScript that targets the framework's runtime primitives. A `.wrn` file declares either a `page` (a route) or a `component` (a reusable, prop-driven fragment) with blocks for `state`, `view` (plain HTML), `seo`, `style`, `functions`, `api`, `ssr`/`client` data bindings, and `realtime` websocket handlers. The pipeline is `source → Lexer → parse() → PageAst → generate() → TypeScript`. It is a build/server-side library — the WrNexus dev loader calls it to compile `.wrn` files on the fly, surfacing `ParseError` as a readable error page.
+23 -4
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/compiler"
},
"homepage": "https://wrnexusjs.dev/packages/compiler",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"compiler"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,10 +37,13 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/syntax": "^0.7.0", "@wrnexus/csr": "^0.8.5",
"@wrnexus/store": "^0.7.0" "@wrnexus/syntax": "^0.8.5",
"@wrnexus/store": "^0.8.5",
"@wrnexus/validation": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+31
View File
@@ -110,6 +110,37 @@ instances. The default store is process-local memory.
(default `console.log`), `requestIdKey` (default `"requestId"`), `now`. (default `console.log`), `requestIdKey` (default `"requestId"`), `now`.
`RequestRecord` = `{ time, id, method, path, status, durationMs }`. `RequestRecord` = `{ time, id, method, path, status, durationMs }`.
### Resilience — `@wrnexus/core`
`resilientCall` standardizes cancellation-aware timeouts, controlled retries,
fixed or exponential backoff, fallback responses, circuit breaking, and bounded
concurrency. Reuse a declarative circuit/bulkhead options object, or an explicit
`CircuitBreaker`/`Bulkhead` instance, wherever calls must share health and
capacity state.
```ts
import { resilientCall } from "@wrnexus/core";
const paymentCircuit = { failures: 5, resetAfter: "30s" } as const;
const status = await resilientCall({
timeout: "5s",
retries: 3,
retryDelay: "100ms",
backoff: "exponential",
circuitBreaker: paymentCircuit,
bulkhead: { concurrency: 20, queue: 100 },
run: (signal) => paymentProvider.checkStatus({ signal }),
fallback: () => ({ state: "unavailable" }),
});
```
`CircuitBreaker.snapshot()` reports `closed`, `open`, or `half-open`, failure
and success counts, and the remaining retry delay for health endpoints and
development tooling. Fail-fast conditions use stable `WRN-RESILIENCE-*` codes.
Core's existing `HealthRegistry`, `withIdempotency`, and pluggable stores/locks
cover health reporting, idempotent requests, and distributed coordination.
### Caching — `@wrnexus/core` ### Caching — `@wrnexus/core`
| Export | Kind | Notes | | Export | Kind | Notes |
+19 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/core"
},
"homepage": "https://wrnexusjs.dev/packages/core",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"core"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -29,6 +45,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+44 -10
View File
@@ -1,5 +1,34 @@
# @wrnexus/csr # @wrnexus/csr
## Navigation state preservation
Pages can opt into restoration across client navigation:
```wrn
page Users {
navigation {
preserve = ["filters", "pagination", "scroll", "tabs", "expanded"]
}
}
```
Form-like categories restore named inputs, selects, and textareas. Password,
file, hidden, CSRF/token/secret/credential fields, and elements marked
`data-no-preserve` are never saved. For tab, expanded, or component UI state,
mark stable elements with `data-wrn-preserve="key"`; their value and ARIA
selected/expanded state are restored. State is scoped to pathname plus query.
## Typed server actions
`createActionClient<Input, Output>(route, name)` supports programmatic calls.
Schema-backed WRN actions also export `__wrnexusActionClients`, whose input and
output are inferred automatically. Enhanced forms expose
`data-wrn-action-state="pending|success|error"` and dispatch bubbling
`wrnexus:action:optimistic`, `:pending`, `:success`, and `:error` events.
Success details contain returned data and invalidated cache tags; error details
contain field errors. Without JavaScript, the same form posts to its page and
receives a 303 redirect or accessible validation response.
> The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms. > The browser-side client runtime for WrNexus — generic, self-contained JS that hydrates server-rendered pages with reactivity, client-side navigation, and realtime rooms.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
@@ -49,16 +78,21 @@ getRealtimeRuntime(): string // → REALTIME_RUNTIME
Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works. Applied to any subtree containing `data-scope`. Expressions are parsed by a tiny eval-free evaluator, so a strict CSP with no `unsafe-eval` works.
| Directive | Purpose | | Directive | Purpose |
| -------------------------------------------------------- | ----------------------------------------------------------------------- | | ---------------------------------- | ---------------------------------------------------- |
| `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree | | `data-scope="count: 0, name: 'x'"` | Declare reactive state on a subtree |
| `data-on-<event>="count++"` | Run a statement in scope on a DOM event | | `data-on-<event>="count++"` | Run a statement in scope on a DOM event |
| `data-text="expr"` | Bind an element's `textContent` to an expression | | `data-text="expr"` | Bind an element's `textContent` to an expression |
| `data-show="expr"` | Toggle visibility (`display`) on truthiness | | `data-show="expr"` | Toggle visibility while preserving interactive state |
| `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder |
| `data-key="item.id"` | Alternative key declaration for `data-for` templates | Compiled conditional rendering and dynamic component cases omit inactive elements from the live
| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values | DOM. `data-show` is a visibility directive for stateful controls and keeps its element mounted.
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) | Neither mechanism is authorization: never place secrets in client-rendered branches. Authorize on
the server and return only data the current request may access.
| `data-for="item in list"` (opt. index and `key item.id`) | Per-item rendering; stable keys preserve DOM identity during reorder |
| `data-key="item.id"` | Alternative key declaration for `data-for` templates |
| `{{expr}}` or `{expr}` | Interpolation inside text nodes and attribute values |
| `data-wrnexus-csr="id"` | Target for a generated CSR fetch binding (fetches `/__wrnexus/csr?...`) |
Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it. Supported expression features: literals, identifiers, member access (`a.b`, `a[b]`), function/method calls, arrays, objects, arithmetic, comparison, equality, logical (`&& ||`), unary (`! - +`), and ternary. Statements support `++`/`--`, assignment operators (`= += -= *= /= %=`), and bare expression/method calls. Rendering is dependency-tracked: a signal change only re-runs the renderers that actually read it.
+20 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.", "description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/csr"
},
"homepage": "https://wrnexusjs.dev/packages/csr",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"csr"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,9 +37,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0" "@wrnexus/core": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+75 -4
View File
@@ -1,5 +1,14 @@
# @wrnexus/db # @wrnexus/db
## Rollout-safe migrations
Run `wrnexus db check` in CI before deployment. The analyzer reports stable
diagnostics for drops, renames, type changes, new/enforced required columns,
and potentially blocking index creation, with an expand/backfill/switch/contract
recommendation. `wrnexus db migrate` blocks critical issues in pending
migrations. `--allow-breaking` is an explicit operator override; already-applied
migrations do not block later releases.
> The database layer for WrNexus: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based `Db` client, migrations, and a sqlc-style query generator. > The database layer for WrNexus: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based `Db` client, migrations, and a sqlc-style query generator.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
@@ -85,7 +94,8 @@ A `Driver` (implemented by adapters) exposes `dialect`, `query(sql, params?)`,
- `exec(sql, params?)``Promise<ExecResult>` (`{ changes, lastInsertId? }`). - `exec(sql, params?)``Promise<ExecResult>` (`{ changes, lastInsertId? }`).
- `tx(fn)` — run `fn(db)` in a transaction; rolls back on throw. Nested `tx` reuses the current transaction. - `tx(fn)` — run `fn(db)` in a transaction; rolls back on throw. Nested `tx` reuses the current transaction.
- `createTable(model)` — runs the model's `CREATE TABLE IF NOT EXISTS` DDL. - `createTable(model)` — runs the model's `CREATE TABLE IF NOT EXISTS` DDL.
- `close()`. - `close()` — idempotently rejects new top-level work, drains active queries and
transactions, then closes the underlying pool.
Every query is parameterized (positional params). `createTableSql(model, dialect, ifNotExists?)` Every query is parameterized (positional params). `createTableSql(model, dialect, ifNotExists?)`
renders `CREATE TABLE` directly; `Dialect` is `"sqlite" | "postgres" | "mysql"`. renders `CREATE TABLE` directly; `Dialect` is `"sqlite" | "postgres" | "mysql"`.
@@ -98,7 +108,9 @@ A process-wide registry the runtime configures at startup from `wrnexus.config.t
- `setDb(db)` / `setDb(name, db)` — set the default or a named connection. - `setDb(db)` / `setDb(name, db)` — set the default or a named connection.
- `registerDb(name, db)` — alias of `setDb(name, db)`. - `registerDb(name, db)` — alias of `setDb(name, db)`.
- `getDb(name = "default")` — the default or a named `Db` (throws if unconfigured). - `getDb(name = "default")` — the default or a named `Db` (throws if unconfigured).
- `hasDb(name?)`, `databaseNames()`, `closeDatabases()`. - `hasDb(name?)`, `databaseNames()`, `closeDatabases()`. Registry shutdown clears
registrations first, attempts every open database, and reports close failures
together with `AggregateError` instead of leaking later pools.
```ts ```ts
const users = await getDb().all("SELECT * FROM users"); const users = await getDb().all("SELECT * FROM users");
@@ -121,8 +133,8 @@ Migrations are `.sql` files (in e.g. `app/db/migrations`), each split into
- `parseMigration(name, content)``Migration` (`{ name, up, down }`). - `parseMigration(name, content)``Migration` (`{ name, up, down }`).
- `loadMigrations(dir)` — parse all `.sql` files, sorted by filename. - `loadMigrations(dir)` — parse all `.sql` files, sorted by filename.
- `appliedMigrations(db)` — applied names, oldest first. - `appliedMigrations(db)` — applied names, oldest first.
- `migrate(db, dir)` — apply all pending (each in a transaction); returns applied names. - `migrate(db, dir, options?)` — apply all pending (each in a transaction); returns applied names.
- `rollback(db, dir)` — roll back the most recent; returns its name or `null`. - `rollback(db, dir, options?)` — roll back the most recent; returns its name or `null`.
- `status(db, dir)``{ name, applied }[]` for every migration file. - `status(db, dir)``{ name, applied }[]` for every migration file.
- `scaffoldMigration(dir, name, dialect, models?)` — write a new numbered migration; with `models` it generates `CREATE`/`DROP` for every table (referenced tables first via topological sort). Returns the file path. - `scaffoldMigration(dir, name, dialect, models?)` — write a new numbered migration; with `models` it generates `CREATE`/`DROP` for every table (referenced tables first via topological sort). Returns the file path.
@@ -199,6 +211,21 @@ const pageTwo = await paginate(
); );
``` ```
For deployments, `{ dryRun: true }` reports pending names without applying
their SQL, `signal` cancels safely between migrations, and the default
database-backed lock prevents concurrent deploy runners. A live lock produces
`WRN-DB-MIGRATION-LOCKED`; crash-stale locks expire after `lockTimeoutMs` (five
minutes by default). Disable it with `lock: false` only when an external deploy
coordinator already guarantees exclusivity.
```ts
const pending = await migrate(db, "app/db/migrations", { dryRun: true });
await migrate(db, "app/db/migrations", {
signal: shutdownController.signal,
lockTimeoutMs: 10 * 60_000,
});
```
MongoDB (document API): MongoDB (document API):
```ts ```ts
@@ -226,3 +253,47 @@ SQL driver — use `@wrnexus/db/mongo` directly.
`wrnexus.config.ts`. `wrnexus.config.ts`.
- The `mongodb` npm package is an optional, lazily-imported peer — install it - The `mongodb` npm package is an optional, lazily-imported peer — install it
only if you use `@wrnexus/db/mongo`. The core package stays dependency-free. only if you use `@wrnexus/db/mongo`. The core package stays dependency-free.
## Repository and transaction helpers
Repositories accept an immutable equality `scope`, normally `{ column: "tenant_id", value:
ctx.tenant.id }`. The scope is injected into every read, count, update and delete, while create
overwrites any caller-supplied tenant value. This makes accidental cross-tenant CRUD through the
repository API fail closed.
```ts
import { createRepository, retryTransaction, databaseHealth, batch } from "@wrnexus/db";
const users = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
});
const user = await users.require(42);
await users.update(42, { active: true });
```
Repository identifiers are validated, writes may be restricted to an allowlist, and values always use query parameters. Infrastructure packages remain helper-only and do not add UI dependencies to server code.
## 0.8 repository and transaction helpers
```ts
import { createRepository, databaseHealth, firstOrThrow, retryTransaction } from "@wrnexus/db";
const usersRepo = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
maxListLimit: 250,
});
const users = await usersRepo.all({
orderBy: "name",
direction: "asc",
limit: 50,
offset: 0,
});
```
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.
`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.
+20 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "Typed database drivers, migrations, instrumentation, repositories, pagination, and transaction helpers.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/db"
},
"homepage": "https://wrnexusjs.dev/packages/db",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"db"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -45,6 +61,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+9 -1
View File
@@ -66,6 +66,12 @@ interface RunningServer {
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running. In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
`getWrnCompileMetrics()` exposes cumulative content-addressed compiler cache
`hits`, `misses`, successful `compilations`, `errors`, `totalDurationMs`, and
`lastDurationMs` for the DevToolbar or custom diagnostics. Tests and embedded
servers can call `resetWrnCompileMetrics()` to establish a fresh measurement
window.
### `createHandlers(deps)` ### `createHandlers(deps)`
The core runtime shared by dev and prod. It handles CORS preflight, `/healthz` and `/__wrnexus/health`, request-body size limits (413), HMR socket upgrades (`/__wrnexus/hmr`), realtime WebSocket upgrades (`defineRoom` default export or a raw `websocket` export), the middleware pipeline, API routes (`/api/*`), framework assets (`/__wrnexus/*`), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip). The core runtime shared by dev and prod. It handles CORS preflight, `/healthz` and `/__wrnexus/health`, request-body size limits (413), HMR socket upgrades (`/__wrnexus/hmr`), realtime WebSocket upgrades (`defineRoom` default export or a raw `websocket` export), the middleware pipeline, API routes (`/api/*`), framework assets (`/__wrnexus/*`), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).
@@ -296,7 +302,9 @@ Pages get only the scripts they use: `nav.js` always, `reactive.js` when a page
- **Bun-only.** Uses `Bun.serve` (HTTP + WebSocket), `Bun.file`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@wrnexus/db`. - **Bun-only.** Uses `Bun.serve` (HTTP + WebSocket), `Bun.file`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@wrnexus/db`.
- Orchestrates the whole framework: `@wrnexus/core` (context, security, realtime registry), `@wrnexus/router`, `@wrnexus/ssr` (`renderDocument`), `@wrnexus/csr` (client runtimes), `@wrnexus/compiler` (`.wrn` → TS), `@wrnexus/styles`, `@wrnexus/ui`, `@wrnexus/validation`, `@wrnexus/i18n`, `@wrnexus/db`, and `@wrnexus/pubsub` (Redis-backed cross-process realtime). - Orchestrates the whole framework: `@wrnexus/core` (context, security, realtime registry), `@wrnexus/router`, `@wrnexus/ssr` (`renderDocument`), `@wrnexus/csr` (client runtimes), `@wrnexus/compiler` (`.wrn` → TS), `@wrnexus/styles`, `@wrnexus/ui`, `@wrnexus/validation`, `@wrnexus/i18n`, `@wrnexus/db`, and `@wrnexus/pubsub` (Redis-backed cross-process realtime).
- `.wrn` files are compiled to TypeScript into a hidden sibling `.wrnexus/` cache dir and dynamically imported; the module cache means each edited server module needs a fresh process (dev) — hence the restart-on-change model. - `.wrn` files compile into a content-addressed hidden `.wrnexus/` cache. Targeted
invalidation gives changed modules a fresh import identity without restarting
the development server.
- Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via `Cache-Control: no-transform`. - Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via `Cache-Control: no-transform`.
</content> </content>
+40 -19
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.", "description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/dev-server"
},
"homepage": "https://wrnexusjs.dev/packages/dev-server",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"dev-server"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -25,25 +41,30 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0", "@wrnexus/authz": "^0.8.5",
"@wrnexus/dev-toolbar": "^0.7.0", "@wrnexus/rpc": "^0.8.5",
"@wrnexus/router": "^0.7.0", "@wrnexus/core": "^0.8.5",
"@wrnexus/ssr": "^0.7.0", "@wrnexus/dev-toolbar": "^0.8.5",
"@wrnexus/csr": "^0.7.0", "@wrnexus/router": "^0.8.5",
"@wrnexus/compiler": "^0.7.0", "@wrnexus/ssr": "^0.8.5",
"@wrnexus/styles": "^0.7.0", "@wrnexus/csr": "^0.8.5",
"@wrnexus/ui": "^0.7.0", "@wrnexus/compiler": "^0.8.5",
"@wrnexus/validation": "^0.7.0", "@wrnexus/styles": "^0.8.5",
"@wrnexus/i18n": "^0.7.0", "@wrnexus/ui": "^0.8.5",
"@wrnexus/db": "^0.7.0", "@wrnexus/validation": "^0.8.5",
"@wrnexus/pubsub": "^0.7.0", "@wrnexus/i18n": "^0.8.5",
"@wrnexus/uploader": "^0.7.0", "@wrnexus/db": "^0.8.5",
"@wrnexus/plugin": "^0.7.0", "@wrnexus/pubsub": "^0.8.5",
"@wrnexus/store": "^0.7.0", "@wrnexus/uploader": "^0.8.5",
"@wrnexus/security": "^0.7.0", "@wrnexus/plugin": "^0.8.5",
"@wrnexus/observability": "^0.7.0" "@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.8.5",
"@wrnexus/observability": "^0.8.5",
"@wrnexus/cache": "^0.8.5",
"@wrnexus/pwa": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+7
View File
@@ -7,6 +7,9 @@ Development-only page quality toolbar for WRNexusJS.
- Runtime, resource and unhandled promise error capture - Runtime, resource and unhandled promise error capture
- Accessibility, SEO, image, media, color, HTML, form, link, responsive and security checks - Accessibility, SEO, image, media, color, HTML, form, link, responsive and security checks
- Performance and network observations - Performance and network observations
- First-class application tabs for runtime, stores, cache, accessibility, SEO, performance,
security, images, links, and JavaScript
- Plugin-contributed applications with badges, descriptions, issue feeds, and structured data
- Element highlighting and issue filtering - Element highlighting and issue filtering
- Server-side issue collector - Server-side issue collector
- Development-only asset strings for direct serving by `@wrnexus/dev-server` - Development-only asset strings for direct serving by `@wrnexus/dev-server`
@@ -21,3 +24,7 @@ Serve `DEV_TOOLBAR_RUNTIME` at `/__wrnexus/dev-toolbar.js` and `DEV_TOOLBAR_CSS`
``` ```
The browser runtime exposes `window.__wrnexusDevToolbar`. The browser runtime exposes `window.__wrnexusDevToolbar`.
Plugin panels returned through `devToolbarPanels()` are automatically added to the application
strip. Their issue category is filterable, and structured `data` is rendered as escaped diagnostic
content so a plugin never needs to inject toolbar HTML.
+19 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/dev-toolbar", "name": "@wrnexus/dev-toolbar",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/dev-toolbar — part of the WrNexus framework.", "description": "@wrnexus/dev-toolbar — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/dev-toolbar"
},
"homepage": "https://wrnexusjs.dev/packages/dev-toolbar",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"dev-toolbar"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -37,6 +53,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+50 -63
View File
@@ -1,80 +1,67 @@
# @wrnexus/encryption # @wrnexus/encryption
> Dependency-free crypto helpers for WrNexus: authenticated symmetric encryption (AES-256-GCM), hashing, and HMAC signing. Authenticated encryption, hashing, HMAC, key rotation, and optional encrypted HTTP exchanges for WRNexusJS.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Core helpers
## Overview - `generateKey()` — random 256-bit AES key encoded as base64.
- `deriveKey(password, salt)` — PBKDF2-derived AES key.
- `encrypt(plaintext, key)` / `decrypt(payload, key)` — AES-256-GCM.
- `sha256(data)` — SHA-256 digest.
- `hmacSign(data, secret)` / `hmacVerify(...)` — HMAC-SHA256.
- `createKeyring(keys)` — active/previous key management.
- `seal()` / `open()` — versioned ciphertext with key ID.
This package provides small, focused cryptographic primitives for server-side use: encrypting secrets/tokens/database fields at rest with AES-256-GCM, deriving keys from passwords via PBKDF2, computing SHA-256 digests, and signing/verifying payloads with HMAC-SHA256. It is built entirely on the standard **Web Crypto API** (`crypto.subtle`) plus `btoa`/`atob` and `TextEncoder`/`TextDecoder` — no third-party dependencies. Reach for it whenever you need to protect sensitive values or verify webhook signatures. All functions are `async` (Web Crypto is promise-based). ## Encrypted HTTP envelope
## Installation
```bash
bun add @wrnexus/encryption
```
> Private package — the machine must be authenticated to the `wrnexus` npm org
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
All keys are exchanged as **base64 strings** and all digests/signatures as **hex strings**.
| Export | Signature | Description |
| ------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `generateKey` | `() => Promise<string>` | Generate a random 256-bit AES key, base64-encoded. Store it as a secret. |
| `deriveKey` | `(password: string, salt: string) => Promise<string>` | Derive a base64 AES-256 key from a password + salt using PBKDF2 (100,000 iterations, SHA-256). |
| `encrypt` | `(plaintext: string, key: string) => Promise<string>` | AES-256-GCM encrypt a string. Returns base64 of `iv(12 bytes) ‖ ciphertext+tag`. A fresh random IV is used each call. |
| `decrypt` | `(payload: string, key: string) => Promise<string>` | Decrypt a value produced by `encrypt`. Throws if the key is wrong or the data was tampered with. |
| `sha256` | `(data: string) => Promise<string>` | SHA-256 hex digest of a string (e.g. content hashing, dedup keys). |
| `hmacSign` | `(data: string, secret: string) => Promise<string>` | HMAC-SHA256 hex signature of `data` with `secret` (e.g. signing webhooks). |
| `hmacVerify` | `(data: string, secret: string, signature: string) => Promise<boolean>` | Constant-time verify of an HMAC-SHA256 hex signature. |
Notes:
- `generateKey` produces a 32-byte (256-bit) key via `crypto.getRandomValues`.
- `encrypt`/`decrypt` require a base64-encoded 256-bit key; anything else throws `"Encryption key must be a base64 256-bit key"`.
- `decrypt` throws `"Invalid ciphertext"` if the payload is shorter than the 12-byte IV, and the underlying Web Crypto call throws on any authentication (tag) mismatch.
- `hmacVerify` compares in constant time (length check plus XOR accumulation) to avoid timing leaks.
## Usage
Symmetric encryption of a secret at rest:
```ts ```ts
import { generateKey, encrypt, decrypt } from "@wrnexus/encryption"; import {
createEncryptedRequest,
createKeyring,
createMemoryReplayStore,
decryptEncryptedResponse,
encryptedExchange,
} from "@wrnexus/encryption";
const key = await generateKey(); // store this safely (env/secret manager) const keyring = createKeyring([{ id: "2026-08", secret: process.env.API_BODY_KEY!, active: true }]);
const box = await encrypt("card #1234", key); // opaque base64 string, safe to persist const replayStore = createMemoryReplayStore();
const plain = await decrypt(box, key); // "card #1234"
// Server middleware.
app.use(
encryptedExchange({
keyring,
replayStore,
maxAgeMs: 60_000,
maxBodyBytes: 1_048_576,
}),
);
// Controlled service/native client.
const request = await createEncryptedRequest(
"https://api.example.com/private/report",
{ reportId: "report-1" },
{ method: "POST", keyring },
);
const response = await fetch(request);
const result = await decryptEncryptedResponse(response, request, { keyring });
``` ```
Deriving a key from a user password instead of a random key: The envelope binds authenticated ciphertext to:
```ts - HTTP method
import { deriveKey, encrypt } from "@wrnexus/encryption"; - URL path and query
- request ID
- timestamp and expiry window
- encryption key ID
- optional replay-store consumption
const key = await deriveKey("correct horse battery staple", "per-user-salt"); `encryptedBody()` decrypts request bodies only. `encryptedExchange()` also encrypts successful downstream responses while allowing application exceptions to propagate normally. `encryptedFetch()` provides a convenient controlled-client call.
const box = await encrypt("secret note", key);
```
Hashing and webhook signature verification: ## Security boundary
```ts Encrypted HTTP bodies **do not replace TLS/HTTPS**. Always use HTTPS.
import { sha256, hmacSign, hmacVerify } from "@wrnexus/encryption";
const digest = await sha256("some content"); // 64-char hex string This layer is appropriate for service-to-service traffic, native/mobile applications, controlled agents, and selected fields protected with server-managed keys. It cannot conceal data from an end user when browser JavaScript receives the decryption key. Never ship a long-lived server encryption key to a browser.
const signature = await hmacSign(rawBody, webhookSecret); Use a shared replay store such as Redis in multi-instance deployments. The memory replay store is process-local.
const ok = await hmacVerify(rawBody, webhookSecret, incomingSignatureHeader);
if (!ok) throw new Error("Invalid webhook signature");
```
## Requirements / Notes
- **Bun-only.** Relies on the Web Crypto API (`crypto.subtle`, `crypto.getRandomValues`) and the global `btoa`/`atob`, `TextEncoder`/`TextDecoder` — all available in Bun's runtime.
- **No dependencies.** The package has an empty dependency set; nothing is bundled beyond standard runtime APIs.
- Algorithms: AES-256-GCM (encryption), PBKDF2 with 100k SHA-256 iterations (key derivation), SHA-256 (digest), HMAC-SHA256 (signing).
- Keep generated/derived keys and HMAC secrets out of source control; treat them as first-class secrets.
+23 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "Authenticated encryption, key rotation, hashing, and optional application-layer encrypted HTTP envelopes.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/encryption"
},
"homepage": "https://wrnexusjs.dev/packages/encryption",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"encryption"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -20,7 +36,11 @@
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+20 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/helpers"
},
"homepage": "https://wrnexusjs.dev/packages/helpers",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"helpers"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,9 +37,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0" "@wrnexus/core": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+60 -134
View File
@@ -1,167 +1,93 @@
# @wrnexus/i18n # @wrnexus/i18n
> Per-request translations plus locale-aware number, date, and currency formatting for WrNexus apps. Recursive locale loading, fallback resolution, SSR/browser translations, locale formatting, and language UI blocks for WRNexusJS.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. ## Locale files
## Overview Both layouts can be used together:
`@wrnexus/i18n` loads locale files from `app/locales/<lang>.json`, resolves the ```text
active language for each request (cookie → `Accept-Language` → default), and app/locales/en.json
builds a `t(key, params)` translator used both in server code and in `.wrn` app/locales/en/common.json
views. It also ships Intl-based formatting helpers and a tiny client runtime that app/locales/en/auth.json
wires up a language switcher. Translation lookup, language resolution, and HTML app/locales/mr/common.json
marker rewriting run server-side; only the small `I18N_RUNTIME` snippet runs in
the browser.
## Installation
```bash
bun add @wrnexus/i18n
``` ```
> Private package — the machine must be authenticated to the `wrnexus` npm org Namespaced files become keys such as `common.save` and `auth.signIn`.
> (a read token in `~/.npmrc`). Requires **Bun** (Node is not supported).
## API
### Loading & resolving
| Export | Signature | Description |
| ------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `loadLocales` | `(dir: string) => Record<string, Messages>` | Reads every `<lang>.json` in `dir` into a `{ lang: messages }` map. Missing dir → `{}`; a bad file is warned and skipped. |
| `resolveI18n` | `(messages: Record<string, Messages>, config?: I18nConfig) => ResolvedI18n` | Merges loaded messages + config into a resolved bundle (default lang, supported langs, messages). |
| `resolveLang` | `(i18n: ResolvedI18n, cookieValue: string \| undefined, acceptLanguage: string \| null) => string` | Picks the active language: matching cookie → best `Accept-Language` tag (falls back to base tag, e.g. `en-US``en`) → `i18n.default`. |
| `makeT` | `(i18n: ResolvedI18n, lang: string) => TFunction` | Builds a translator resolving current language → default → the key itself, with `{param}` interpolation. |
### Types & constants
| Export | Kind | Notes |
| -------------- | ----------- | ------------------------------------------------------------------------------ |
| `Messages` | `type` | `Record<string, unknown>` — a locale's messages (supports nested/dotted keys). |
| `I18nConfig` | `interface` | `{ default?: string; locales?: string[] }`. |
| `ResolvedI18n` | `interface` | `{ default: string; langs: string[]; messages: Record<string, Messages> }`. |
| `LANG_COOKIE` | `const` | `"wire-lang"` — the cookie the language is read from / written to. |
| `I18N_JS_HREF` | `const` | `"/__wrnexus/i18n.js"` — URL the client runtime is served at. |
### HTML & client runtime
| Export | Signature | Description |
| ---------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `translateHtml` | `(html: string, t: TFunction) => string` | Rewrites markers in rendered HTML: `t:<attr>="key"``<attr>="<translation>"` (attribute-escaped) and `<tag data-t="key">…</tag>` → element text becomes the translation (HTML-escaped). No-op unless a marker is present. |
| `renderI18nData` | `(i18n: ResolvedI18n, lang: string) => string` | JS snippet setting `window.__wireI18n = { lang, langs, default }` for the client switcher. |
| `I18N_RUNTIME` | `const string` | Browser IIFE that binds `[data-wire-lang-set="es"]` clicks and `select[data-wire-lang]` changes to set the `wire-lang` cookie and reload. Exposes `window.__wireLang.set(lang)`. |
### Formatting helpers (re-exported from `./format.ts`)
| Export | Signature | Example |
| -------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------- |
| `formatNumber` | `(value: number, lang: string, options?: Intl.NumberFormatOptions) => string` | `1234.5 → "1,234.5"` |
| `formatCurrency` | `(value: number, currency: string, lang: string) => string` | `9.99, "USD" → "$9.99"` |
| `formatDate` | `(value: Date \| number \| string, lang: string, options?: Intl.DateTimeFormatOptions) => string` | defaults to `{ dateStyle: "medium" }` |
| `formatRelativeTime` | `(value: number, unit: Intl.RelativeTimeFormatUnit, lang: string) => string` | `-3, "day" → "3 days ago"` (`numeric: "auto"`) |
| `plural` | `(count: number, forms: Partial<Record<Intl.LDMLPluralRule, string>>, lang: string) => string` | picks CLDR form; `#` is replaced by `count` |
## Usage
### Server: load, resolve, translate
```ts ```ts
import { import { loadLocales, makeT, resolveI18n, resolveLang } from "@wrnexus/i18n";
loadLocales,
resolveI18n,
resolveLang,
makeT,
translateHtml,
LANG_COOKIE,
} from "@wrnexus/i18n";
// app/locales/en.json, app/locales/es.json const i18n = resolveI18n(loadLocales("app/locales", { strict: true }), {
const messages = loadLocales("app/locales"); default: "en",
const i18n = resolveI18n(messages, { default: "en", locales: ["en", "es"] }); locales: ["en", "mr", "hi"],
fallbacks: { "mr-IN": ["mr", "en"] },
cookie: { name: "wire-lang", sameSite: "Lax", secure: true },
});
// Per request: const lang = resolveLang(i18n, cookieValue, request.headers.get("accept-language"));
const lang = resolveLang(i18n, req.cookies?.[LANG_COOKIE], req.headers.get("accept-language"));
const t = makeT(i18n, lang); const t = makeT(i18n, lang);
t("common.hello", { name: "Ajay" });
t("nav.home"); // dotted key → "Home"
t("greeting", { name: "Ada" }); // "Hello, {name}" → "Hello, Ada"
// After rendering a .wrn view, resolve translation markers in the HTML:
const finalHtml = translateHtml(renderedHtml, t);
``` ```
`app/locales/en.json`: ## Resolution behavior
```json - normalized BCP-47-style locale names
{ - cookie preference
"nav": { "home": "Home" }, - weighted `Accept-Language`
"greeting": "Hello, {name}" - wildcard language ranges
} - regional base fallback
``` - explicit fallback chains
- configured default language
- automatic RTL for Arabic, Hebrew, Persian, Urdu, and related languages
### Views: translation markers Locale JSON is size-limited and rejects prototype-pollution keys. Recursive namespace collisions are resolved safely.
## Views and runtime
```html ```html
<h1 data-t="nav.home">Home</h1> <h1 data-t="dashboard.title">Dashboard</h1>
<input t:placeholder="search.placeholder" /> <input t:placeholder="search.placeholder" />
``` ```
`translateHtml` replaces the element text for `data-t` and the attribute value for Text and translated attributes are resolved during SSR. Active/fallback messages are serialized safely for the language runtime, which rebinds `data-t` markers after client navigation.
any `t:<attr>` (e.g. `t:placeholder`, `t:aria-label`).
### Client: language switcher Enable `i18nPlugin()` to use:
```ts - `<LanguageSwitcher />`
import { renderI18nData, I18N_RUNTIME, I18N_JS_HREF } from "@wrnexus/i18n"; - `<LocaleStatus />`
// In the document <head>: `LanguageSwitcher` renders a native `select[data-wire-lang]`. The packaged runtime validates the
const head = ` selection against the configured locales, writes the configured language cookie, updates the
<script>${renderI18nData(i18n, lang)}</script> document `lang`/`dir` attributes, emits `wrnexus:language-change`, and reloads so the next SSR
<script src="${I18N_JS_HREF}"></script> request uses the same cookie. No application-owned browser script is required.
`;
// Serve I18N_RUNTIME at I18N_JS_HREF; then in markup: ## Formatting
// <button data-wire-lang-set="es">Español</button>
// <select data-wire-lang>…</select>
```
### Formatting - `formatNumber`
- `formatCurrency`
- `formatDate`
- `formatRelativeTime`
- `plural`
- `createLocaleFormatter`
- `translationCoverage`
Localization tooling can extract statically discoverable `t("key")`,
`i18n.t("key")`, and `data-i18n="key"` usage, compare every locale with a
reference, and create layout-stressing pseudo-locales:
```ts ```ts
import { import {
formatNumber, auditLocaleKeys,
formatCurrency, createPseudoLocale,
formatDate, extractTranslationKeysFromFiles,
formatRelativeTime,
plural,
} from "@wrnexus/i18n"; } from "@wrnexus/i18n";
formatNumber(1234.5, lang); // "1,234.5" const used = extractTranslationKeysFromFiles(sourceFiles);
formatCurrency(9.99, "USD", lang); // "$9.99" const coverage = auditLocaleKeys(messages, "en");
formatDate(Date.now(), lang); // "Jul 4, 2026" const enXA = createPseudoLocale(messages.en);
formatRelativeTime(-3, "day", lang); // "3 days ago" const arXB = createPseudoLocale(messages.en, { rtl: true });
plural(2, { one: "# item", other: "# items" }, lang); // "2 items"
``` ```
## Configuration Pseudo-localization preserves interpolation placeholders and markup tags. RTL
pseudo output uses Unicode direction controls, while runtime direction detection
`resolveI18n` accepts an `I18nConfig`: continues to derive `rtl` from Arabic and other RTL language subtags.
- `default` — fallback language; used when nothing else matches. Ignored if it has
no loaded messages, in which case the first supported language is used.
- `locales` — explicit supported-language list; defaults to the loaded locale names.
Language resolution order at request time (`resolveLang`): a supported `wire-lang`
cookie value → the first matching `Accept-Language` tag (or its base subtag) → the
resolved default.
## Requirements / Notes
- **Bun-only.** Locale loading uses `node:fs` (`existsSync`, `readdirSync`,
`readFileSync`) and `node:path`; formatting relies on the platform `Intl` APIs.
- Works with [`@wrnexus/core`](../core) — `TFunction` (the `t(key, params)` type)
comes from core, and the resolved translator is exposed as `ctx.t` / `ctx.lang`
in request handling.
- Nested message objects are supported: keys are looked up whole first, then split
on `.` to walk the object tree.
+37 -5
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.", "description": "Locale loading, fallback resolution, SSR/browser translations, formatters, and WRNexusJS language components.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/i18n"
},
"homepage": "https://wrnexusjs.dev/packages/i18n",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"i18n"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,12 +34,28 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} },
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./components/*": "./components/*"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0" "@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
},
"wrnexus": {
"plugin": {
"plugin": "./dist/plugin.js",
"export": "default",
"factory": true
}
}, },
"files": [ "files": [
"dist" "dist",
"README.md",
"components"
] ]
} }
+59
View File
@@ -130,3 +130,62 @@ app.use(jwtAuth({ secret: process.env.JWT_SECRET!, required: false }));
- Integrates with [`@wrnexus/core`](../core) for `Context`, `Middleware`, and - Integrates with [`@wrnexus/core`](../core) for `Context`, `Middleware`, and
`ctx.user`; it complements the framework's cookie/session auth with a `ctx.user`; it complements the framework's cookie/session auth with a
stateless bearer-token flow for API and mobile clients. stateless bearer-token flow for API and mobile clients.
## Access, refresh, scope, and cookie helpers
```ts
import {
createAccessToken,
createRefreshToken,
verifyAccessToken,
verifyRefreshToken,
extractBearerToken,
requireScopes,
jwtCookie,
} from "@wrnexus/jwt";
```
The helpers add explicit `type: "access" | "refresh"` claims, scope checks, refresh-token family metadata, no-store token responses, and secure cookie defaults. `__Host-` cookies are rejected unless they use `Path=/` and `Secure`; `SameSite=None` is rejected without `Secure`.
## 0.8 helper kit
```ts
import {
createTokenPair,
verifyAccessToken,
verifyRefreshToken,
extractBearerToken,
readJwtCookie,
jwtCookie,
clearJwtCookie,
requireScopes,
} from "@wrnexus/jwt";
const pair = await createTokenPair(user.id, {
accessSecret: process.env.JWT_ACCESS_SECRET!,
refreshSecret: process.env.JWT_REFRESH_SECRET!,
scopes: ["profile:read"],
family: sessionFamily,
});
```
The helper kit validates `__Host-` cookie invariants, cookie names and paths, `SameSite=None` security, typed access/refresh token types, scope requirements, and no-store token responses.
In addition to local HS256 secrets/keyrings, the package verifies standards-based
RS256 tokens through bounded remote JWKS caches:
```ts
import { createRemoteJwks, verifyJwtWithJwks } from "@wrnexus/jwt";
const jwks = createRemoteJwks("https://issuer.example/.well-known/jwks.json");
const claims = await verifyJwtWithJwks(token, jwks, {
issuer: "https://issuer.example",
audience: "my-api",
maxAge: 300,
});
```
JWKS URLs must use HTTPS. Responses have key-count/byte limits, accept only
RS256 signing RSA keys, deduplicate concurrent refreshes, cache imported public
keys, and force an immediate refresh for an unknown `kid` so issuer rotation
does not wait for cache expiry. Never use decoded-but-unverified claims for an
authorization decision.
+23 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "HS256 JSON Web Tokens, key rotation, access/refresh helpers, scopes, cookies, and auth middleware.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/jwt"
},
"homepage": "https://wrnexusjs.dev/packages/jwt",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"jwt"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -20,7 +36,11 @@
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+8
View File
@@ -76,6 +76,14 @@ const status = network ? await network.getStatus() : { connected: true, connecti
Unavailable required plugins throw `MobileUnavailableError` with an actionable message. Unavailable required plugins throw `MobileUnavailableError` with an actionable message.
The package also provides portable application-facing primitives:
- `listenDeepLinks` normalizes initial and live links with an allowed-scheme list.
- `PushNotifications` performs permission gating and validates registrations.
- `SecureStorage` namespaces and validates keys over an application-supplied encrypted
Keychain/Keystore adapter; it does not mislabel browser `localStorage` as secure.
- `OfflineQueue` persists bounded sync batches through a pluggable durable store.
## Requirements / Notes ## Requirements / Notes
- Capacitor plugin imports must remain in browser-owned modules. - Capacitor plugin imports must remain in browser-owned modules.
+20 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.", "description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/mobile"
},
"homepage": "https://wrnexusjs.dev/packages/mobile",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"mobile"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,9 +37,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/native": "^0.7.0" "@wrnexus/native": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+3
View File
@@ -79,6 +79,9 @@ const position = await native.run(
Built-ins include `camera`, `clipboard.write`, `share`, `geolocation`, `network`, Built-ins include `camera`, `clipboard.write`, `share`, `geolocation`, `network`,
`haptics`, storage, filesystem, notifications, and device information. `haptics`, storage, filesystem, notifications, and device information.
`defineNativeManifest` declares required capabilities and typed permissions, while
`PermissionManager` normalizes permission query/request flows across platform adapters.
## Requirements / Notes ## Requirements / Notes
Use `supports()` before showing optional controls. Mobile capabilities require their Use `supports()` before showing optional controls. Mobile capabilities require their
+19 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.", "description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/native"
},
"homepage": "https://wrnexusjs.dev/packages/native",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"native"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -29,6 +45,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+21
View File
@@ -194,3 +194,24 @@ const gitlab = defineProvider({
`verifier` between `startAuth` and `completeAuth` (session or signed cookie). `verifier` between `startAuth` and `completeAuth` (session or signed cookie).
- Pairs with [`@wrnexus/core`](../core) — feed the normalized `OAuthProfile` into - Pairs with [`@wrnexus/core`](../core) — feed the normalized `OAuthProfile` into
`logIn` to establish a session. `logIn` to establish a session.
OIDC integrations can combine strict discovery with the rotating JWKS verifier:
```ts
import { createRemoteJwks } from "@wrnexus/jwt";
import { discoverOidc, verifyOidcIdToken } from "@wrnexus/oauth";
const metadata = await discoverOidc("https://issuer.example");
const jwks = createRemoteJwks(metadata.jwks_uri);
const claims = await verifyOidcIdToken(idToken, {
issuer: metadata.issuer,
clientId: "client-id",
jwks,
nonce: expectedNonce,
accessToken,
});
```
Discovery requires an exact normalized issuer and HTTPS endpoints without URL
credentials/fragments. ID-token verification checks the RS256 signature,
expiry/not-before, issuer, audience, required OIDC claims, nonce, multi-audience
`azp`, optional token age, and optional `at_hash` binding.
+22 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.", "description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/oauth"
},
"homepage": "https://wrnexusjs.dev/packages/oauth",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"oauth"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -20,7 +36,11 @@
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"dependencies": {
"@wrnexus/jwt": "^0.8.5"
},
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+53
View File
@@ -1,7 +1,60 @@
# @wrnexus/plugin # @wrnexus/plugin
## Least-privilege package permissions
Package manifests declare every framework capability they register:
```json
{
"wrnexus": {
"permissions": ["routes", "migrations"],
"routes": [{ "kind": "api", "path": "/api/example", "entry": "./route.ts" }]
}
}
```
Applications can enable fail-closed grants:
```ts
export default {
pluginPermissions: {
enforce: true,
grants: { "example-plugin": ["routes"] },
},
};
```
Discovery rejects used-but-undeclared capabilities with
`WRN-PLUGIN-PERMISSION-UNDECLARED` and ungranted capabilities with
`WRN-PLUGIN-PERMISSION-DENIED`. Permissions cover components, browser runtime,
assets, styles, routes, middleware, migrations, config, transforms,
diagnostics/tooling, and server/build hooks.
## Compatibility matrices
Manifests can add `compatibility: { bunMin: "1.3.0", os: ["linux",
"darwin"] }` alongside `runtimes` and `requires`. Use
`testPluginCompatibility(manifest, targets)` in a package test to exercise the
complete support matrix. Runtime discovery enforces the same Bun minimum, OS,
runtime, and capability declarations used by the test kit.
Deterministic WRNexusJS plugin contracts for configuration, AST/code transforms, Deterministic WRNexusJS plugin contracts for configuration, AST/code transforms,
diagnostics, development servers, production builds, and DevToolbar extensions. diagnostics, development servers, production builds, and DevToolbar extensions.
Use `definePlugin()` and declare `enforce`, `before`, or `after` when ordering matters. Use `definePlugin()` and declare `enforce`, `before`, or `after` when ordering matters.
Duplicate names and dependency cycles are rejected. Duplicate names and dependency cycles are rejected.
## Complete lifecycle and contributions
Plugins may implement `setup`, `configure`, `configResolved`, `transformAst`,
`transformCode`, `diagnostics`, `routes`, `configureServer`, `buildStart`,
`buildEnd`, `render`, `deploy`, `shutdown`, and `hmrUpdate`. The runner preserves
resolved plugin order for every hook and executes `setup` exactly once.
In addition to components, routes, middleware, assets, styles, runtimes, and
migrations, plugins can contribute `directives`, `cliCommands`,
`virtualModules`, `deploymentAdapters`, `configSchemas`, `documentation`, and
`typeDefinitions`. Names are collision checked. Configuration schemas run after
configuration resolution, CLI commands are callable as normal `wrnexus`
commands, directives participate in AST transformation, and production builds
materialize virtual modules and invoke matching contributed adapters.
+20 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/plugin", "name": "@wrnexus/plugin",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/plugin — part of the WrNexus framework.", "description": "@wrnexus/plugin — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/plugin"
},
"homepage": "https://wrnexusjs.dev/packages/plugin",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"plugin"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -33,9 +49,10 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/syntax": "^0.7.0" "@wrnexus/syntax": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+9 -4
View File
@@ -31,13 +31,15 @@ Creates a bus over a driver. Defaults to `memoryDriver()` (in-process).
interface PubSub { interface PubSub {
publish<T = unknown>(topic: string, message: T): Promise<void>; publish<T = unknown>(topic: string, message: T): Promise<void>;
subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void; subscribe<T = unknown>(pattern: string, handler: Handler<T>): () => void;
close(): Promise<void>;
} }
type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>; type Handler<T = unknown> = (message: T, topic: string) => void | Promise<void>;
``` ```
- `publish(topic, message)` — resolves once the driver has dispatched the message. - `publish(topic, message)` — resolves once the driver and in-memory async handlers finish.
- `subscribe(pattern, handler)` — returns an unsubscribe function. - `subscribe(pattern, handler)` — returns an unsubscribe function.
- `close()` — idempotently rejects new work, clears local subscriptions, and closes the driver.
### Pattern matching ### Pattern matching
@@ -67,7 +69,7 @@ then `redis://localhost:6379`. The URL may carry a password and a database index
(e.g. `redis://:secret@host:6379/2`). (e.g. `redis://:secret@host:6379/2`).
```ts ```ts
function redisDriver(url?: string): PubSubDriver & { close(): void }; function redisDriver(url?: string, options?: RedisDriverOptions): PubSubDriver & { close(): void };
``` ```
- Exact topics use Redis `SUBSCRIBE`; wildcard patterns (`ns:*`, `*`) use - Exact topics use Redis `SUBSCRIBE`; wildcard patterns (`ns:*`, `*`) use
@@ -75,6 +77,9 @@ function redisDriver(url?: string): PubSubDriver & { close(): void };
- Messages are JSON-stringified on publish and `JSON.parse`d on receipt; a payload - Messages are JSON-stringified on publish and `JSON.parse`d on receipt; a payload
that isn't valid JSON is delivered as the raw string. that isn't valid JSON is delivered as the raw string.
- `close()` tears down both the subscriber and publisher connections. - `close()` tears down both the subscriber and publisher connections.
- Lost sockets reconnect with bounded exponential backoff and active subscriptions
are replayed. `maxPending` bounds unavailable-connection writes (default 1000);
`reconnectDelayMs` and `reconnectMaxDelayMs` tune recovery (100ms/5000ms).
### RESP codec (internal) ### RESP codec (internal)
@@ -115,8 +120,8 @@ bus.subscribe("order:*", (msg, topic) => {
await bus.publish("order:created", { id: 7 }); await bus.publish("order:created", { id: 7 });
// on shutdown // on shutdown (also closes the driver)
driver.close(); await bus.close();
``` ```
## Requirements / Notes ## Requirements / Notes
+23 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.", "description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/pubsub"
},
"homepage": "https://wrnexusjs.dev/packages/pubsub",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"pubsub"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -19,12 +35,17 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
}, },
"./brokers": {
"types": "./dist/brokers.d.ts",
"import": "./dist/brokers.js"
},
"./redis": { "./redis": {
"types": "./dist/redis.d.ts", "types": "./dist/redis.d.ts",
"import": "./dist/redis.js" "import": "./dist/redis.js"
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+61 -14
View File
@@ -43,33 +43,45 @@ function createQueue(options?: QueueOptions): Queue;
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. | | `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). | | `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. | | `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
| `capacity` | `number` | unlimited | Maximum queued plus active jobs before adds reject. |
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. | | `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
### `Queue` ### `Queue`
The object returned by `createQueue`. The object returned by `createQueue`.
| Method | Signature | Description | | Method | Signature | Description |
| --------- | -------------------------------------------------------------- | -------------------------------------------------------------- | | ---------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| `add` | `add<T>(name, data: T, options?: AddOptions): Promise<Job<T>>` | Enqueue a job under a worker name. Returns the created job. | | `add` | `add<T>(name, data: T, options?: AddOptions): Promise<Job<T>>` | Enqueue a job under a worker name. Returns the created job. |
| `process` | `process<T>(name, handler: JobHandler<T>): void` | Register the worker that runs jobs of the given name. | | `process` | `process<T>(name, handler: JobHandler<T>): void` | Register the worker that runs jobs of the given name. |
| `drain` | `drain(now?: number): Promise<number>` | Run every job whose `runAt ≤ now`, once. Returns how many ran. | | `drain` | `drain(now?: number): Promise<number>` | Run every job whose `runAt ≤ now`, once. Returns how many ran. |
| `start` | `start(): void` | Begin polling every `pollMs`. No-op if already started. | | `start` | `start(): void` | Begin polling every `pollMs`. No-op if already started. |
| `stop` | `stop(): void` | Stop the poll timer. | | `stop` | `stop(): void` | Stop the poll timer. |
| `size` | `size(): number` | Number of jobs currently queued. | | `shutdown` | `shutdown({ force? }): Promise<void>` | Stop accepting jobs and await active work; force aborts it. |
| `size` | `size(): number` | Number of jobs currently queued. |
| `get/list` | `get(id)` / `list(name?)` | Inspect defensive copies of pending jobs. |
| `cancel` | `cancel(id): boolean` | Remove queued work or abort an active handler. |
| `failed` | `failed(): Job[]` | Inspect exhausted jobs in the dead-letter collection. |
| `retry` | `retry(id): Promise<boolean>` | Reset and requeue a dead-lettered job. |
#### `AddOptions` #### `AddOptions`
| Option | Type | Description | | Option | Type | Description |
| ------------- | -------- | -------------------------------------------------------------------------- | | ---------------- | -------- | -------------------------------------------------------------------------- |
| `delayMs` | `number` | Delay before the job becomes runnable (ms). | | `delayMs` | `number` | Delay before the job becomes runnable (ms). |
| `maxAttempts` | `number` | Max attempts before dead-lettering. Defaults to the queue's `maxAttempts`. | | `maxAttempts` | `number` | Max attempts before dead-lettering. Defaults to the queue's `maxAttempts`. |
| `repeat` | `number` | Re-enqueue this job this many ms after each successful run (recurring). | | `repeat` | `number` | Re-enqueue this job this many ms after each successful run (recurring). |
| `priority` | `number` | Higher values are selected first among due jobs. |
| `idempotencyKey` | `string` | Return the matching pending job instead of enqueueing a duplicate. |
#### `JobHandler<T>` #### `JobHandler<T>`
```ts ```ts
type JobHandler<T = unknown> = (job: Job<T>) => void | Promise<void>; type JobHandler<T = unknown> = (
job: Job<T>,
context: { signal: AbortSignal },
) => void | Promise<void>;
``` ```
#### `Job<T>` #### `Job<T>`
@@ -106,6 +118,18 @@ await queue.add("email", { to: "a@b.com" }, { delayMs: 5000, maxAttempts: 3 });
queue.start(); // begin polling; queue.stop() to halt queue.start(); // begin polling; queue.stop() to halt
``` ```
Use `context.signal` in network/database calls so forced shutdown and active
cancellation finish promptly. For process termination, prefer
`await queue.shutdown()`; use `{ force: true }` only after your grace period.
### Durable queue
`createDurableQueue({ store })` retains jobs until their handler succeeds and
supports atomic leases when a driver implements `QueueStore.claim`. It exposes
the same cancellation/shutdown behavior plus `list`, `failed`, and `retry`.
The included `memoryQueueStore()` is useful for tests; production Redis/SQL
drivers should make `claim()` atomic to prevent two workers executing one job.
### Recurring jobs ### Recurring jobs
Pass `repeat` to re-enqueue a job a fixed interval after each successful run: Pass `repeat` to re-enqueue a job a fixed interval after each successful run:
@@ -145,6 +169,29 @@ clock = 5000;
const ran = await queue.drain(); // => 1 const ran = await queue.drain(); // => 1
``` ```
### Durable workflows and approvals
`createWorkflowEngine(store)` executes dependency-ordered steps and persists every transition,
result, progress update, failure, cancellation, and approval record. Approval steps pause safely
and can resume after a process restart because the snapshot lives in the supplied `WorkflowStore`.
```ts
const workflow = defineDurableWorkflow({
name: "publish-report",
steps: [
{ name: "build", run: buildReport },
{ name: "approve", dependsOn: ["build"], approval: true, run: (report) => report },
{ name: "publish", dependsOn: ["approve"], run: publishReport },
],
});
const run = await engine.start(workflow, input);
await engine.approve(workflow, run.id, "approve", currentUser.id);
```
Use `memoryWorkflowStore()` for tests. Production stores implement the small `get`, `put`, and
`list` contract using the same transactional database or durable service as the application.
## Retry & backoff behavior ## Retry & backoff behavior
- On a thrown handler error, the job is retried while `attempts < maxAttempts`. - On a thrown handler error, the job is retried while `attempts < maxAttempts`.
+22 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.", "description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/queue"
},
"homepage": "https://wrnexusjs.dev/packages/queue",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"queue"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -20,7 +36,11 @@
"import": "./dist/index.js" "import": "./dist/index.js"
} }
}, },
"dependencies": {
"@wrnexus/core": "^0.8.5"
},
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+19 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/reactive"
},
"homepage": "https://wrnexusjs.dev/packages/reactive",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"reactive"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,6 +37,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+21 -4
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.", "description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/router"
},
"homepage": "https://wrnexusjs.dev/packages/router",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"router"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,10 +37,11 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/compiler": "^0.7.0", "@wrnexus/compiler": "^0.8.5",
"@wrnexus/core": "^0.7.0" "@wrnexus/core": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+22 -5
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.", "description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ssr"
},
"homepage": "https://wrnexusjs.dev/packages/ssr",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ssr"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -29,11 +45,12 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0", "@wrnexus/core": "^0.8.5",
"@wrnexus/store": "^0.7.0", "@wrnexus/store": "^0.8.5",
"@wrnexus/security": "^0.7.0" "@wrnexus/security": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+21
View File
@@ -1,5 +1,26 @@
# @wrnexus/styles # @wrnexus/styles
## Reusable layers and presets
Compose local or package foundations in order; later layers override earlier
ones and the application has final base-config precedence:
```ts
export default defineConfig({
extends: ["@workroot/wrnexus-enterprise", "./layers/company"],
profiles: { production: { port: 8080 } },
});
```
A directory layer exports `wrnexus.layer.ts` (JavaScript/MJS are supported).
A package can provide that conventional file or declare
`wrnexus.layer` in its `package.json`. Layers may extend other layers and carry
the complete app configuration, including plugins that contribute layouts,
components, routes, middleware, and migrations. `plugins` and `head` compose;
other arrays intentionally replace earlier values. Cycles and missing/invalid
entries fail with stable `WRN-CONFIG-LAYER-*` diagnostics. `wrnexus config
--explain` lists every resolved layer source.
> Global CSS bundling, the `--wire-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps. > Global CSS bundling, the `--wire-*` design-token theme system, and the `wrnexus.config.ts` app-config loader for WrNexus apps.
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
+22 -5
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.", "description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/styles"
},
"homepage": "https://wrnexusjs.dev/packages/styles",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"styles"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,11 +37,12 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.7.0", "@wrnexus/uploader": "^0.8.5",
"@wrnexus/core": "^0.7.0", "@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.7.0" "@wrnexus/plugin": "^0.8.5"
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+23 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/syntax", "name": "@wrnexus/syntax",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/syntax — part of the WrNexus framework.", "description": "@wrnexus/syntax — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/syntax"
},
"homepage": "https://wrnexusjs.dev/packages/syntax",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"syntax"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -38,9 +54,14 @@
"./spec": { "./spec": {
"types": "./dist/spec.d.ts", "types": "./dist/spec.d.ts",
"import": "./dist/spec.js" "import": "./dist/spec.js"
},
"./formatter": {
"types": "./dist/formatter.d.ts",
"import": "./dist/formatter.js"
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+16
View File
@@ -103,6 +103,22 @@ Remember to `await app.close()` when done.
## Usage ## Usage
The CLI supports focused suites by file or directory convention:
```bash
wrnexus test unit # *.unit.test.ts or test/unit/**
wrnexus test component # *.component.test.ts or test/component/**
wrnexus test api # *.api.test.ts or test/api/**
wrnexus test accessibility # *.a11y.test.ts / *.accessibility.test.ts
wrnexus test performance # *.performance.test.ts / *.benchmark.test.ts
wrnexus test browser # Playwright project when configured
wrnexus test visual # Playwright tests tagged @visual
```
Pass the application directory after the level, for example
`wrnexus test component examples/basic-app`. A focused command fails clearly when no matching
suite exists instead of silently running unrelated tests.
```ts ```ts
import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test"; import { test, expect, renderComponent, mountHtml, createHarness } from "@wrnexus/test";
+19 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/test"
},
"homepage": "https://wrnexusjs.dev/packages/test",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"test"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,6 +37,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+19 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/tracking"
},
"homepage": "https://wrnexusjs.dev/packages/tracking",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"tracking"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -21,6 +37,7 @@
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md"
] ]
} }
+16 -16
View File
@@ -359,6 +359,15 @@ Reusable preference switcher component.
- Slots: None - Slots: None
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])` - Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
### Toaster
Reusable toaster component.
- Mount: `data-component="Toaster"`
- Props: `color: string = "info"`, `size: string = "default"`, `position: string = "bottom-right"`, `duration: number = 4500`, `max: number = 4`, `pauseOnHover: boolean = true`, `showIcon: boolean = true`, `successIcon: string = ""`, `dangerIcon: string = ""`, `warningIcon: string = ""`, `infoIcon: string = ""`, `closable: boolean = true`, `showProgress: boolean = true`, `closeLabel: string = "Dismiss notification"`, `class: string = ""`
- Slots: None
- Outputs: `show({ id: number; message: string; tone: string })`, `dismiss({ id: number; reason: string })`, `action({ id: number; sourceEvent: Event })`
## Data ## Data
### MetricCard ### MetricCard
@@ -554,15 +563,6 @@ Theme-aware, responsive data map component.
- Slots: `default` - Slots: `default`
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)` - Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### DataTable
Theme-aware, responsive data table component.
- Mount: `data-component="DataTable"`
- Props: `size: string = "default"`, `color: string = "primary"`, `caption: string = "Data Table"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `striped: boolean = true`, `class: string = ""`
- Slots: `default`
- Outputs: `sort({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `change({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `rowClick({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `pageChange({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
### DragAndDrop ### DragAndDrop
Theme-aware, responsive drag and drop component. Theme-aware, responsive drag and drop component.
@@ -945,7 +945,7 @@ Open an accessible keyboard-aware action menu from pointer or keyboard context i
Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events. Present responsive modal side or bottom content with focus management, backdrop behavior, slots, and close events.
- Mount: `data-component="Drawer"` - Mount: `data-component="Drawer"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""` - Props: `open: boolean = false`, `defaultOpen: boolean = false`, `placement: string = "right"`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `title: string = "Drawer"`, `description: string = ""`, `icon: string = ""`, `label: string = "Drawer"`, `closeLabel: string = "Close drawer"`, `showClose: boolean = true`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `duration: number = 260`, `overlay: boolean = true`, `scrollable: boolean = true`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer` - Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ placement: string; sourceEvent: Event })`, `close({ reason: string; placement: string; sourceEvent: Event })`, `cancel({ placement: string; sourceEvent: Event })` - Outputs: `open({ placement: string; sourceEvent: Event })`, `close({ reason: string; placement: string; sourceEvent: Event })`, `cancel({ placement: string; sourceEvent: Event })`
@@ -963,7 +963,7 @@ Open an accessible anchored menu with keyboard navigation, item selection, actio
Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing. Present an accessible modal dialog with focus management, confirmation, cancellation, slots, and responsive sizing.
- Mount: `data-component="Modal"` - Mount: `data-component="Modal"`
- Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `class: string = ""` - Props: `open: boolean = false`, `defaultOpen: boolean = false`, `title: string = "Modal"`, `description: string = ""`, `icon: string = ""`, `label: string = "Modal dialog"`, `size: string = "md"`, `placement: string = "center"`, `color: string = "primary"`, `variant: string = "default"`, `showClose: boolean = true`, `closeLabel: string = "Close modal"`, `closeOnBackdrop: boolean = true`, `closeOnEscape: boolean = true`, `closeOnCancel: boolean = true`, `closeOnConfirm: boolean = false`, `showFooter: boolean = true`, `cancelLabel: string = "Cancel"`, `cancelIcon: string = ""`, `confirmLabel: string = "Confirm"`, `confirmIcon: string = ""`, `confirmDisabled: boolean = false`, `confirmLoading: boolean = false`, `destructive: boolean = false`, `triggerLabel: string = ""`, `triggerIcon: string = ""`, `scrollable: boolean = true`, `scrollBehavior: string = "inside"`, `class: string = ""`
- Slots: `trigger`, `header`, `default`, `footer` - Slots: `trigger`, `header`, `default`, `footer`
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `cancel({ sourceEvent: Event })`, `confirm({ sourceEvent: Event })` - Outputs: `open({ sourceEvent: Event })`, `close({ reason: string; sourceEvent: Event })`, `cancel({ sourceEvent: Event })`, `confirm({ sourceEvent: Event })`
@@ -987,11 +987,11 @@ Show concise accessible contextual help on hover, focus, click, or controlled op
## Tables ## Tables
### Table ### DataTable
Theme-aware, responsive table component. Sortable, filterable, paginated data table with row selection.
- Mount: `data-component="Table"` - Mount: `data-component="DataTable"`
- Props: `size: string = "default"`, `color: string = "primary"`, `caption: string = "Table"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `striped: boolean = true`, `class: string = ""` - Props: `color: string = "primary"`, `size: string = "default"`, `columns: unknown[] = []`, `rows: unknown[] = []`, `rowKey: string = "id"`, `remote: boolean = false`, `loadingLabel: string = "Loading"`, `errorLabel: string = "Could not load this data"`, `retryLabel: string = "Try again"`, `caption: string = ""`, `description: string = ""`, `searchable: boolean = true`, `searchPlaceholder: string = "Search"`, `paginated: boolean = true`, `pageSize: number = 10`, `paginationStyle: string = "compact"`, `pageSizes: number[] = [10, 25, 50]`, `selectable: boolean = false`, `actions: unknown[] = []`, `striped: boolean = true`, `bordered: boolean = true`, `gridlines: string = "rows"`, `density: string = "default"`, `emptyLabel: string = "No records to show"`, `noResultsLabel: string = "No records match your search"`, `clearSearchLabel: string = "Clear search"`, `stickyFirstColumn: boolean = false`, `layout: string = "rows"`, `class: string = ""`
- Slots: `default` - Slots: `default`
- Outputs: `sort({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `rowClick({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)` - Outputs: `sort({ key: string; direction: string })`, `search({ query: string })`, `pageChange({ page: number; pageSize: number })`, `select({ selected: Array<string | number>; all: boolean })`, `change({ page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string })`, `rowClick({ row: object; sourceEvent: Event })`, `action({ id: string; selected: Array<string | number>; rows: object[]; sourceEvent: Event })`, `request({ instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string })`
+7 -7
View File
@@ -168,8 +168,8 @@
}, },
{ {
"name": "DataTable", "name": "DataTable",
"category": "integrations", "category": "tables",
"purpose": "Theme-aware, responsive data table component." "purpose": "Sortable, filterable, paginated data table with row selection."
}, },
{ {
"name": "DatePicker", "name": "DatePicker",
@@ -471,11 +471,6 @@
"category": "forms", "category": "forms",
"purpose": "Theme-aware, responsive switch component." "purpose": "Theme-aware, responsive switch component."
}, },
{
"name": "Table",
"category": "tables",
"purpose": "Theme-aware, responsive table component."
},
{ {
"name": "Tabs", "name": "Tabs",
"category": "navigation", "category": "navigation",
@@ -511,6 +506,11 @@
"category": "integrations", "category": "integrations",
"purpose": "Theme-aware, responsive toast notifications component." "purpose": "Theme-aware, responsive toast notifications component."
}, },
{
"name": "Toaster",
"category": "core",
"purpose": "Reusable toaster component."
},
{ {
"name": "ToggleCount", "name": "ToggleCount",
"category": "advanced-forms", "category": "advanced-forms",
+343 -98
View File
@@ -4569,16 +4569,9 @@
{ {
"name": "DataTable", "name": "DataTable",
"mount": "DataTable", "mount": "DataTable",
"category": "integrations", "category": "tables",
"purpose": "Theme-aware, responsive data table component.", "purpose": "Sortable, filterable, paginated data table with row selection.",
"props": [ "props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{ {
"name": "color", "name": "color",
"type": "string", "type": "string",
@@ -4587,10 +4580,10 @@
"options": [] "options": []
}, },
{ {
"name": "caption", "name": "size",
"type": "string", "type": "string",
"required": false, "required": false,
"default": "\"Data Table\"", "default": "\"default\"",
"options": [] "options": []
}, },
{ {
@@ -4607,6 +4600,111 @@
"default": "[]", "default": "[]",
"options": [] "options": []
}, },
{
"name": "rowKey",
"type": "string",
"required": false,
"default": "\"id\"",
"options": []
},
{
"name": "remote",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "loadingLabel",
"type": "string",
"required": false,
"default": "\"Loading\"",
"options": []
},
{
"name": "errorLabel",
"type": "string",
"required": false,
"default": "\"Could not load this data\"",
"options": []
},
{
"name": "retryLabel",
"type": "string",
"required": false,
"default": "\"Try again\"",
"options": []
},
{
"name": "caption",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "description",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "searchable",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "searchPlaceholder",
"type": "string",
"required": false,
"default": "\"Search\"",
"options": []
},
{
"name": "paginated",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "pageSize",
"type": "number",
"required": false,
"default": "10",
"options": []
},
{
"name": "paginationStyle",
"type": "string",
"required": false,
"default": "\"compact\"",
"options": []
},
{
"name": "pageSizes",
"type": "number[]",
"required": false,
"default": "[10, 25, 50]",
"options": []
},
{
"name": "selectable",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "actions",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{ {
"name": "striped", "name": "striped",
"type": "boolean", "type": "boolean",
@@ -4614,6 +4712,62 @@
"default": "true", "default": "true",
"options": [] "options": []
}, },
{
"name": "bordered",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "gridlines",
"type": "string",
"required": false,
"default": "\"rows\"",
"options": []
},
{
"name": "density",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "emptyLabel",
"type": "string",
"required": false,
"default": "\"No records to show\"",
"options": []
},
{
"name": "noResultsLabel",
"type": "string",
"required": false,
"default": "\"No records match your search\"",
"options": []
},
{
"name": "clearSearchLabel",
"type": "string",
"required": false,
"default": "\"Clear search\"",
"options": []
},
{
"name": "stickyFirstColumn",
"type": "boolean",
"required": false,
"default": "false",
"options": []
},
{
"name": "layout",
"type": "string",
"required": false,
"default": "\"rows\"",
"options": []
},
{ {
"name": "class", "name": "class",
"type": "string", "type": "string",
@@ -4626,26 +4780,47 @@
"outputs": [ "outputs": [
{ {
"name": "sort", "name": "sort",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" "payloadType": "{ key: string; direction: string }"
}, },
{ {
"name": "select", "name": "search",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" "payloadType": "{ query: string }"
},
{
"name": "change",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
},
{
"name": "rowClick",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
}, },
{ {
"name": "pageChange", "name": "pageChange",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null" "payloadType": "{ page: number; pageSize: number }"
},
{
"name": "select",
"payloadType": "{ selected: Array<string | number>; all: boolean }"
},
{
"name": "change",
"payloadType": "{ page: number; pageSize: number; total: number; query: string; sortKey: string; sortDirection: string }"
},
{
"name": "rowClick",
"payloadType": "{ row: object; sourceEvent: Event }"
},
{
"name": "action",
"payloadType": "{ id: string; selected: Array<string | number>; rows: object[]; sourceEvent: Event }"
},
{
"name": "request",
"payloadType": "{ instanceId: number; page: number; pageSize: number; sortKey: string; sortDirection: string; query: string }"
} }
], ],
"events": ["sort", "select", "change", "rowClick", "pageChange"], "events": [
"sort",
"search",
"pageChange",
"select",
"change",
"rowClick",
"action",
"request"
],
"source": "components/DataTable.wrn" "source": "components/DataTable.wrn"
}, },
{ {
@@ -5220,6 +5395,13 @@
"default": "true", "default": "true",
"options": [] "options": []
}, },
{
"name": "duration",
"type": "number",
"required": false,
"default": "260",
"options": []
},
{ {
"name": "overlay", "name": "overlay",
"type": "boolean", "type": "boolean",
@@ -8736,6 +8918,13 @@
"default": "true", "default": "true",
"options": [] "options": []
}, },
{
"name": "scrollBehavior",
"type": "string",
"required": false,
"default": "\"inside\"",
"options": []
},
{ {
"name": "class", "name": "class",
"type": "string", "type": "string",
@@ -12009,80 +12198,6 @@
"events": ["input", "change", "focus", "blur"], "events": ["input", "change", "focus", "blur"],
"source": "components/Switch.wrn" "source": "components/Switch.wrn"
}, },
{
"name": "Table",
"mount": "Table",
"category": "tables",
"purpose": "Theme-aware, responsive table component.",
"props": [
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "color",
"type": "string",
"required": false,
"default": "\"primary\"",
"options": []
},
{
"name": "caption",
"type": "string",
"required": false,
"default": "\"Table\"",
"options": []
},
{
"name": "columns",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "rows",
"type": "unknown[]",
"required": false,
"default": "[]",
"options": []
},
{
"name": "striped",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "class",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
}
],
"slots": ["default"],
"outputs": [
{
"name": "sort",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
},
{
"name": "select",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
},
{
"name": "rowClick",
"payloadType": "{ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null"
}
],
"events": ["sort", "select", "rowClick"],
"source": "components/table.wrn"
},
{ {
"name": "Tabs", "name": "Tabs",
"mount": "Tabs", "mount": "Tabs",
@@ -12893,6 +13008,136 @@
"events": ["add", "dismiss", "clear", "action"], "events": ["add", "dismiss", "clear", "action"],
"source": "components/ToastNotifications.wrn" "source": "components/ToastNotifications.wrn"
}, },
{
"name": "Toaster",
"mount": "Toaster",
"category": "core",
"purpose": "Reusable toaster component.",
"props": [
{
"name": "color",
"type": "string",
"required": false,
"default": "\"info\"",
"options": []
},
{
"name": "size",
"type": "string",
"required": false,
"default": "\"default\"",
"options": []
},
{
"name": "position",
"type": "string",
"required": false,
"default": "\"bottom-right\"",
"options": []
},
{
"name": "duration",
"type": "number",
"required": false,
"default": "4500",
"options": []
},
{
"name": "max",
"type": "number",
"required": false,
"default": "4",
"options": []
},
{
"name": "pauseOnHover",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "showIcon",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "successIcon",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "dangerIcon",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "warningIcon",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "infoIcon",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
},
{
"name": "closable",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "showProgress",
"type": "boolean",
"required": false,
"default": "true",
"options": []
},
{
"name": "closeLabel",
"type": "string",
"required": false,
"default": "\"Dismiss notification\"",
"options": []
},
{
"name": "class",
"type": "string",
"required": false,
"default": "\"\"",
"options": []
}
],
"slots": [],
"outputs": [
{
"name": "show",
"payloadType": "{ id: number; message: string; tone: string }"
},
{
"name": "dismiss",
"payloadType": "{ id: number; reason: string }"
},
{
"name": "action",
"payloadType": "{ id: number; sourceEvent: Event }"
}
],
"events": ["show", "dismiss", "action"],
"source": "components/Toaster.wrn"
},
{ {
"name": "ToggleCount", "name": "ToggleCount",
"mount": "ToggleCount", "mount": "ToggleCount",
+7 -2
View File
@@ -45,8 +45,12 @@ component ContextMenu {
sourceEvent.preventDefault() sourceEvent.preventDefault()
} }
if (placement === "pointer" && sourceEvent) { if (placement === "pointer" && sourceEvent) {
positionX = Math.max(12, Math.min(sourceEvent.clientX || 12, window.innerWidth - 340)) // Place the menu at the pointer and let the anchored clamp in the
positionY = Math.max(12, Math.min(sourceEvent.clientY || 12, window.innerHeight - 420)) // runtime pull it back on screen once it has been laid out and can
// actually be measured. Subtracting a guessed 340x420 here instead
// pushed every menu that was not that size away from the pointer.
positionX = Math.max(12, sourceEvent.clientX || 12)
positionY = Math.max(12, sourceEvent.clientY || 12)
} }
visible = true visible = true
output.open({ output.open({
@@ -161,6 +165,7 @@ component ContextMenu {
<div <div
class="wire-context-menu__panel" class="wire-context-menu__panel"
data-wrn-anchored="true"
data-show='{open || visible}' data-show='{open || visible}'
role="menu" role="menu"
aria-label='{label}' aria-label='{label}'
File diff suppressed because it is too large Load Diff
+83 -1
View File
@@ -20,6 +20,8 @@ open: boolean = false
showClose: boolean = true showClose: boolean = true
closeOnBackdrop: boolean = true closeOnBackdrop: boolean = true
closeOnEscape: boolean = true closeOnEscape: boolean = true
// Open/close animation length in ms. 0 disables the animation entirely.
duration: number = 260
overlay: boolean = true overlay: boolean = true
scrollable: boolean = true scrollable: boolean = true
triggerLabel: string = "" triggerLabel: string = ""
@@ -79,7 +81,9 @@ open: boolean = false
data-overlay='{overlay ? "true" : "false"}' data-overlay='{overlay ? "true" : "false"}'
data-scrollable='{scrollable ? "true" : "false"}' data-scrollable='{scrollable ? "true" : "false"}'
class='wire-drawer {class}' class='wire-drawer {class}'
style='--drawer-duration: {duration}ms'
> >
{#if triggerLabel} {#if triggerLabel}
<button <button
type="button" type="button"
@@ -101,7 +105,6 @@ open: boolean = false
<div <div
class="wire-drawer__layer" class="wire-drawer__layer"
data-show='{open || visible}'
role="presentation" role="presentation"
@keydown='handleKeydown(event)' @keydown='handleKeydown(event)'
> >
@@ -218,12 +221,32 @@ open: boolean = false
cursor: pointer; cursor: pointer;
} }
/*
* The layer stays in the layout and is revealed by [data-open]; it used to
* be toggled with data-show, which sets display:none, and display cannot
* be transitioned -- the drawer simply snapped in and out. visibility is
* delayed by the duration on the way out so the panel can finish sliding
* before the layer is taken out of the hit-testing tree.
*/
.wire-drawer__layer { .wire-drawer__layer {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 1200; z-index: 1200;
display: flex; display: flex;
pointer-events: none; pointer-events: none;
visibility: hidden;
opacity: 0;
transition:
opacity var(--drawer-duration, 260ms) ease,
visibility 0s linear var(--drawer-duration, 260ms);
}
.wire-drawer[data-open="true"] .wire-drawer__layer {
visibility: visible;
opacity: 1;
transition:
opacity var(--drawer-duration, 260ms) ease,
visibility 0s linear 0s;
} }
.wire-drawer__backdrop { .wire-drawer__backdrop {
@@ -262,6 +285,38 @@ open: boolean = false
box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent); box-shadow: -28px 0 80px color-mix(in srgb, black 28%, transparent);
pointer-events: auto; pointer-events: auto;
overflow: hidden; overflow: hidden;
/* Slides in from whichever edge the placement puts it on. */
transform: translateX(100%);
transition: transform var(--drawer-duration, 260ms) cubic-bezier(0.32, 0.72, 0, 1);
}
.wire-drawer[data-open="true"] .wire-drawer__panel {
transform: none;
}
.wire-drawer[data-placement="left"] .wire-drawer__panel {
transform: translateX(-100%);
}
.wire-drawer[data-placement="top"] .wire-drawer__panel {
transform: translateY(-100%);
}
.wire-drawer[data-placement="bottom"] .wire-drawer__panel {
transform: translateY(100%);
}
.wire-drawer[data-open="true"][data-placement="left"] .wire-drawer__panel,
.wire-drawer[data-open="true"][data-placement="top"] .wire-drawer__panel,
.wire-drawer[data-open="true"][data-placement="bottom"] .wire-drawer__panel {
transform: none;
}
@media (prefers-reduced-motion: reduce) {
.wire-drawer__layer,
.wire-drawer__panel {
transition: none;
}
} }
.wire-drawer[data-size="sm"] .wire-drawer__panel { .wire-drawer[data-size="sm"] .wire-drawer__panel {
@@ -395,12 +450,19 @@ open: boolean = false
color: color-mix(in srgb, currentColor 76%, transparent); color: color-mix(in srgb, currentColor 76%, transparent);
} }
/*
* padding is reset explicitly: an app-level `button { padding: ... }` rule
* outranks the browser default and leaves this fixed-size button with a
* content box of a couple of pixels, which squeezes the icon to a sliver
* and reads as "the close button has no icon". Same trap as Modal.
*/
.wire-drawer__close { .wire-drawer__close {
appearance: none; appearance: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex: 0 0 auto; flex: 0 0 auto;
padding: 0;
width: 2.35rem; width: 2.35rem;
height: 2.35rem; height: 2.35rem;
color: var(--wire-color-text-muted); color: var(--wire-color-text-muted);
@@ -417,10 +479,30 @@ open: boolean = false
outline: none; outline: none;
} }
/* Never let the glyph be shrunk by the flex container. */
.wire-drawer__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
}
/*
* Slot content is authored by the host app, so the app global stylesheet
* styles it too. A bare element selector there (p { color: ... }) beats
* anything the panel merely *inherits*, which is how modal body copy ended
* up muted grey on a saturated background. State the colour explicitly;
* :where() keeps the specificity low enough that any class the app puts on
* its own slot content still wins.
*/
.wire-drawer__body { .wire-drawer__body {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
padding: 1.35rem; padding: 1.35rem;
color: var(--wire-color-text);
}
.wire-drawer__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
color: inherit;
} }
.wire-drawer[data-scrollable="true"] .wire-drawer__body { .wire-drawer[data-scrollable="true"] .wire-drawer__body {
+1
View File
@@ -171,6 +171,7 @@ items: unknown[] = []
<div <div
class="wire-dropdown__panel" class="wire-dropdown__panel"
data-wrn-anchored="true"
data-show='{open || visible}' data-show='{open || visible}'
role="menu" role="menu"
aria-label='{menuLabel}' aria-label='{menuLabel}'
+197 -18
View File
@@ -35,6 +35,7 @@ component Modal {
triggerLabel: string = "" triggerLabel: string = ""
triggerIcon: string = "" triggerIcon: string = ""
scrollable: boolean = true scrollable: boolean = true
scrollBehavior: string = "inside"
class: string = "" class: string = ""
} }
@@ -50,6 +51,16 @@ component Modal {
output.open({ sourceEvent: sourceEvent }) output.open({ sourceEvent: sourceEvent })
} }
// Slot content can close the modal it sits in by dispatching a bubbling
// wrnexus:modal:close event, e.g. from a form success handler:
//
// event.target.dispatchEvent(
// new CustomEvent("wrnexus:modal:close", { bubbles: true })
// )
//
// The listener is on the modal root, so the event only ever closes the
// modal the dispatching element is actually inside -- no ids to wire up
// and no way to close somebody else's modal by accident.
client function hideModal(reason, sourceEvent) { client function hideModal(reason, sourceEvent) {
visible = false visible = false
output.close({ output.close({
@@ -87,21 +98,23 @@ component Modal {
<div <div
{...attrs} {...attrs}
data-ui-component="Modal" data-ui-component="Modal"
data-open='{open || visible ? "true" : "false"}' data-open='{isOpen() ? "true" : "false"}'
data-size='{size}' data-size='{size}'
data-placement='{placement}' data-placement='{placement}'
data-color='{color}' data-color='{color}'
data-variant='{variant}' data-variant='{variant}'
data-scrollable='{scrollable ? "true" : "false"}' data-scrollable='{scrollable ? "true" : "false"}'
data-scroll='{scrollBehavior}'
data-destructive='{destructive ? "true" : "false"}' data-destructive='{destructive ? "true" : "false"}'
class='wire-modal {class}' class='wire-modal {class}'
@wrnexus:modal:close='hideModal("api", event)'
> >
{#if triggerLabel} {#if triggerLabel}
<button <button
type="button" type="button"
class="wire-modal__trigger" class="wire-modal__trigger"
aria-haspopup="dialog" aria-haspopup="dialog"
aria-expanded='{open || visible ? "true" : "false"}' aria-expanded='{isOpen() ? "true" : "false"}'
@click='showModal(event)' @click='showModal(event)'
> >
{#if triggerIcon} {#if triggerIcon}
@@ -127,7 +140,7 @@ component Modal {
<div <div
class="wire-modal__layer" class="wire-modal__layer"
data-show='{open || visible}' data-show="isOpen()"
role="presentation" role="presentation"
@keydown='handleKeydown(event)' @keydown='handleKeydown(event)'
> >
@@ -183,11 +196,20 @@ component Modal {
aria-label='{closeLabel}' aria-label='{closeLabel}'
@click='hideModal("close-button", event)' @click='hideModal("close-button", event)'
> >
<span <svg
class="icon-[lucide--x]" viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true" aria-hidden="true"
> >
</span> <path d="M18 6 6 18" />
<path d="M6 6 18 18" />
</svg>
</button> </button>
{/if} {/if}
</header> </header>
@@ -277,16 +299,26 @@ component Modal {
--modal-contrast: var(--wire-color-secondary-contrast); --modal-contrast: var(--wire-color-secondary-contrast);
} }
/*
* Fallbacks below (the second var() argument): the theme token generator
* (packages/styles/src/theme.ts) only emits a real -contrast token for
* primary and secondary. info, success, and danger have no contrast
* token defined at all, so var(--wire-color-info-contrast) with no
* fallback resolves to nothing, and --modal-contrast becomes invalid --
* which made confirm-button and solid-panel text unreadable. White is a
* safe default against these saturated colors until the theme package
* defines real tokens for them.
*/
.wire-modal[data-color="info"] { .wire-modal[data-color="info"] {
--modal-accent: var(--wire-color-info); --modal-accent: var(--wire-color-info);
--modal-soft: var(--wire-color-info-soft); --modal-soft: var(--wire-color-info-soft);
--modal-contrast: var(--wire-color-info-contrast); --modal-contrast: var(--wire-color-info-contrast, white);
} }
.wire-modal[data-color="success"] { .wire-modal[data-color="success"] {
--modal-accent: var(--wire-color-success); --modal-accent: var(--wire-color-success);
--modal-soft: var(--wire-color-success-soft); --modal-soft: var(--wire-color-success-soft);
--modal-contrast: var(--wire-color-success-contrast); --modal-contrast: var(--wire-color-success-contrast, white);
} }
.wire-modal[data-color="warning"] { .wire-modal[data-color="warning"] {
@@ -299,7 +331,7 @@ component Modal {
.wire-modal[data-destructive="true"] { .wire-modal[data-destructive="true"] {
--modal-accent: var(--wire-color-danger); --modal-accent: var(--wire-color-danger);
--modal-soft: var(--wire-color-danger-soft); --modal-soft: var(--wire-color-danger-soft);
--modal-contrast: var(--wire-color-on-danger); --modal-contrast: var(--wire-color-on-danger, white);
} }
.wire-modal__trigger, .wire-modal__trigger,
@@ -321,8 +353,35 @@ component Modal {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 650; font-weight: 650;
cursor: pointer; cursor: pointer;
transition: opacity 150ms ease, transform 150ms ease, box-shadow 150ms ease;
} }
.wire-modal__trigger:hover {
opacity: 0.92;
}
.wire-modal__trigger:active {
transform: scale(0.97);
}
.wire-modal__trigger:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--modal-accent) 40%, transparent);
}
/*
* Hidden by default so the SSR-rendered HTML never paints the layer before
* hydration runs. data-open on the wire-modal root is evaluated and
* serialized to a real true or false string at render time -- a plain
* bind, not the raw-expression data-show directive -- so this selector
* is correct on first paint with zero flash, with no dependency on
* client JS having run yet. The data-show attribute and client directive
* still run after hydration to keep things in sync for state changes,
* but visibility itself is driven by CSS here. visibility (rather than
* display) is used so the open and close transitions below can actually
* animate -- a box that starts at display: none has no prior frame to
* transition from.
*/
.wire-modal__layer { .wire-modal__layer {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -331,6 +390,15 @@ component Modal {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 1rem; padding: 1rem;
visibility: hidden;
opacity: 0;
transition: opacity 180ms ease, visibility 0s linear 180ms;
}
.wire-modal[data-open="true"] .wire-modal__layer {
visibility: visible;
opacity: 1;
transition: opacity 180ms ease, visibility 0s linear 0s;
} }
.wire-modal[data-placement="top"] .wire-modal__layer { .wire-modal[data-placement="top"] .wire-modal__layer {
@@ -338,6 +406,12 @@ component Modal {
padding-top: clamp(1rem, 8vh, 5rem); padding-top: clamp(1rem, 8vh, 5rem);
} }
.wire-modal[data-scroll="page"] .wire-modal__layer {
align-items: flex-start;
overflow-y: auto;
padding: 2.5rem 1rem;
}
.wire-modal__backdrop { .wire-modal__backdrop {
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -370,6 +444,18 @@ component Modal {
0 1px 0 color-mix(in srgb, white 5%, transparent) inset, 0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
0 40px 110px color-mix(in srgb, black 38%, transparent); 0 40px 110px color-mix(in srgb, black 38%, transparent);
overflow: hidden; overflow: hidden;
opacity: 0;
transform: scale(0.96) translateY(10px);
transition: opacity 180ms ease, transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}
.wire-modal[data-open="true"] .wire-modal__panel {
opacity: 1;
transform: none;
}
.wire-modal[data-size="xs"] .wire-modal__panel {
width: min(19rem, calc(100vw - 2rem));
} }
.wire-modal[data-size="sm"] .wire-modal__panel { .wire-modal[data-size="sm"] .wire-modal__panel {
@@ -390,6 +476,10 @@ component Modal {
max-height: none; max-height: none;
} }
.wire-modal[data-scroll="page"] .wire-modal__panel {
max-height: none;
}
.wire-modal[data-variant="soft"] .wire-modal__panel { .wire-modal[data-variant="soft"] .wire-modal__panel {
background: background:
linear-gradient(145deg, var(--modal-soft), transparent 68%), linear-gradient(145deg, var(--modal-soft), transparent 68%),
@@ -447,6 +537,7 @@ component Modal {
} }
.wire-modal__heading-copy h2 { .wire-modal__heading-copy h2 {
color: inherit;
font-size: 1.08rem; font-size: 1.08rem;
font-weight: 650; font-weight: 650;
line-height: 1.3; line-height: 1.3;
@@ -462,32 +553,100 @@ component Modal {
color: color-mix(in srgb, currentColor 76%, transparent); color: color-mix(in srgb, currentColor 76%, transparent);
} }
/*
* padding is reset explicitly: an app-level `button { padding: … }` rule
* outranks the browser default, and 1rem of horizontal padding left this
* 2.15rem button with a ~2px content box -- which squeezed the icon to
* 0.4px wide and read as "the close button has no icon".
*/
.wire-modal__close { .wire-modal__close {
appearance: none; appearance: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex: 0 0 auto; flex: 0 0 auto;
width: 2.35rem; padding: 0;
height: 2.35rem; width: 2.15rem;
height: 2.15rem;
color: var(--wire-color-text-muted); color: var(--wire-color-text-muted);
background: var(--wire-color-surface-soft); background: transparent;
border: 1px solid var(--wire-color-border); border: 1px solid transparent;
border-radius: 0.75rem; border-radius: 9999px;
cursor: pointer; cursor: pointer;
transition: background 150ms ease, color 150ms ease, border-color 150ms ease, transform 150ms ease;
}
/*
* On a solid panel the background is the accent color, so the muted-grey
* default is close to invisible -- the dismiss affordance reads as
* missing rather than subtle. Derive it from the panel contrast color
* instead, and give it a faint ring so it is unmistakably a control.
*/
.wire-modal[data-variant="solid"] .wire-modal__close {
color: color-mix(in srgb, currentColor 82%, transparent);
border-color: color-mix(in srgb, currentColor 35%, transparent);
}
.wire-modal[data-variant="solid"] .wire-modal__close:hover {
color: currentColor;
background: color-mix(in srgb, black 18%, transparent);
border-color: color-mix(in srgb, currentColor 55%, transparent);
}
.wire-modal__close:hover {
color: var(--wire-color-text);
background: var(--wire-color-surface-soft);
} }
.wire-modal__close:hover,
.wire-modal__close:focus-visible { .wire-modal__close:focus-visible {
color: var(--modal-accent); color: var(--modal-accent);
border-color: color-mix(in srgb, var(--modal-accent) 34%, var(--wire-color-border)); border-color: color-mix(in srgb, var(--modal-accent) 45%, transparent);
outline: none; outline: none;
} }
.wire-modal__close:active {
transform: scale(0.92);
}
/* Never let the glyph be shrunk by the flex container. */
.wire-modal__close svg {
flex: 0 0 auto;
width: 1rem;
height: 1rem;
}
/*
* Slot content is authored by the host app, so the app global stylesheet
* styles it too. A bare element selector there (p { color: ... }) beats
* anything the panel merely *inherits*, which is how solid-variant modals
* ended up with muted grey body copy on a saturated accent background --
* unreadable, and worst exactly where contrast matters most (the
* destructive confirm). Setting the color on the body makes the panel
* choice explicit instead of leaving it to inheritance.
*
* NOTE: apostrophes are avoided in .wrn style comments on purpose -- the
* block scanner treats a quote as a string delimiter while it counts
* braces, so a stray one breaks parsing of the whole component.
*/
.wire-modal__body { .wire-modal__body {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
padding: 1.4rem; padding: 1.4rem;
color: var(--wire-color-text);
}
/*
* :where() keeps this at the specificity of .wire-modal__body alone, so it
* outranks a global element selector but still yields to any class the app
* puts on its own slot content (an error message, a muted caption). A
* plain .wire-modal__body p list would have quietly overridden those.
*/
.wire-modal__body :where(p, li, dd, dt, h1, h2, h3, h4, h5, h6, span, label, code) {
color: inherit;
}
.wire-modal[data-variant="solid"] .wire-modal__body {
color: var(--modal-contrast);
} }
.wire-modal[data-scrollable="true"] .wire-modal__body { .wire-modal[data-scrollable="true"] .wire-modal__body {
@@ -548,6 +707,18 @@ component Modal {
border: 1px solid transparent; border: 1px solid transparent;
} }
/*
* On a solid-variant panel the panel background is also --modal-accent,
* so a plain primary button (same color) has no visible edge against it.
* Darken the fill slightly and add a light border so the button still
* reads as a distinct, clickable pill instead of blending into the panel.
*/
.wire-modal[data-variant="solid"] .wire-modal__button--primary {
background: color-mix(in srgb, black 18%, var(--modal-accent));
border-color: color-mix(in srgb, white 32%, transparent);
box-shadow: 0 1px 0 color-mix(in srgb, white 12%, transparent) inset;
}
.wire-modal__button:disabled { .wire-modal__button:disabled {
opacity: 0.55; opacity: 0.55;
cursor: not-allowed; cursor: not-allowed;
@@ -575,6 +746,7 @@ component Modal {
} }
.wire-modal__panel, .wire-modal__panel,
.wire-modal[data-size="xs"] .wire-modal__panel,
.wire-modal[data-size="sm"] .wire-modal__panel, .wire-modal[data-size="sm"] .wire-modal__panel,
.wire-modal[data-size="lg"] .wire-modal__panel, .wire-modal[data-size="lg"] .wire-modal__panel,
.wire-modal[data-size="xl"] .wire-modal__panel { .wire-modal[data-size="xl"] .wire-modal__panel {
@@ -599,10 +771,17 @@ component Modal {
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.wire-modal__button, .wire-modal__button,
.wire-modal__spinner { .wire-modal__spinner,
.wire-modal__layer,
.wire-modal__panel,
.wire-modal__close {
animation: none; animation: none;
transition: none; transition: none;
} }
.wire-modal__panel {
transform: none;
}
} }
} }
} }
+8 -1
View File
@@ -123,12 +123,13 @@ component Popover {
<section <section
class="wire-popover__panel" class="wire-popover__panel"
data-wrn-anchored="true"
data-show='{open || visible}' data-show='{open || visible}'
role="dialog" role="dialog"
aria-label='{title || triggerLabel}' aria-label='{title || triggerLabel}'
> >
{#if showArrow} {#if showArrow}
<span class="wire-popover__arrow" aria-hidden="true"></span> <span class="wire-popover__arrow" data-wrn-anchor-arrow="true" aria-hidden="true"></span>
{/if} {/if}
{#if title || description || icon || showClose} {#if title || description || icon || showClose}
@@ -471,7 +472,13 @@ component Popover {
color: color-mix(in srgb, currentColor 76%, transparent); color: color-mix(in srgb, currentColor 76%, transparent);
} }
/*
* padding is reset explicitly: an app-level `button { padding: ... }` rule
* outranks the browser default and crushes the icon inside this
* fixed-size button. Same trap as Modal and Drawer.
*/
.wire-popover__close { .wire-popover__close {
padding: 0;
appearance: none; appearance: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
-20
View File
@@ -1,20 +0,0 @@
component Table {
outputs {
sort(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
select(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
rowClick(payload: { value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)
}
props {
size: string = "default"
color: string = "primary"
caption: string = "Table"
columns: unknown[] = []
rows: unknown[] = []
striped: boolean = true
class: string = ""
}
view {
<div class="wire-next wire-next--color-{color} wire-next--size-{size} wire-next--table {class}"><table><caption>{caption}</caption><thead><tr>{#each columns as column}<th>{column.label}</th>{/each}</tr></thead><tbody>{#each rows as row}<tr>{#each columns as column}<td>{row[column.key]}</td>{/each}</tr>{/each}</tbody></table><slot /></div>
}
}
+2 -1
View File
@@ -97,11 +97,12 @@ id: string = ""
<span <span
id='{id}' id='{id}'
class="wire-tooltip__content" class="wire-tooltip__content"
data-wrn-anchored="true"
data-show='{open || visible}' data-show='{open || visible}'
role="tooltip" role="tooltip"
> >
{#if showArrow} {#if showArrow}
<span class="wire-tooltip__arrow" aria-hidden="true"></span> <span class="wire-tooltip__arrow" data-wrn-anchor-arrow="true" aria-hidden="true"></span>
{/if} {/if}
{#if title} {#if title}
+23 -2
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/ui"
},
"homepage": "https://wrnexusjs.dev/packages/ui",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"ui"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -19,6 +35,10 @@
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
}, },
"./registry": {
"types": "./dist/registry.d.ts",
"import": "./dist/registry.js"
},
"./components/*": "./components/*", "./components/*": "./components/*",
"./component-catalog.json": "./component-catalog.json", "./component-catalog.json": "./component-catalog.json",
"./component-migrations.json": "./component-migrations.json", "./component-migrations.json": "./component-migrations.json",
@@ -26,10 +46,11 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0" "@wrnexus/core": "^0.8.5"
}, },
"files": [ "files": [
"dist", "dist",
"README.md",
"components", "components",
"ui.css", "ui.css",
"component-catalog.json", "component-catalog.json",
-22
View File
@@ -8037,28 +8037,6 @@
cursor: pointer; cursor: pointer;
} }
.wire-next--table {
width: 100%;
overflow: auto;
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius-md);
}
.wire-next--table table {
width: 100%;
border-collapse: collapse;
}
.wire-next--table caption,
.wire-next--table th,
.wire-next--table td {
padding: 0.75rem;
border-bottom: 1px solid var(--wire-color-border);
text-align: left;
}
.wire-next--table caption,
.wire-next--table th {
font-weight: 800;
}
@keyframes wire-shimmer { @keyframes wire-shimmer {
to { to {
background-position: -200% 0; background-position: -200% 0;
+38
View File
@@ -108,6 +108,11 @@ export const GET = serveFromStore("docs"); // your middleware decides who gets i
## API ## API
Uploads can participate in security and media pipelines without changing storage drivers. Pass a
`scan` hook to reject malware/DLP findings before storage, and `afterStore` to enqueue image/video
processing or indexing. If post-processing throws, WRNexus deletes the newly written object so a
partially accepted upload is never left behind.
| Export | What | | Export | What |
| --------------------------------------- | --------------------------------------------------------------- | | --------------------------------------- | --------------------------------------------------------------- |
| `handleUpload(opts)` | POST route handler → JSON `{ ok, files }` | | `handleUpload(opts)` | POST route handler → JSON `{ ok, files }` |
@@ -123,3 +128,36 @@ export const GET = serveFromStore("docs"); // your middleware decides who gets i
- SigV4 signing is implemented from scratch (no `@aws-sdk`); tested against local S3 semantics. - SigV4 signing is implemented from scratch (no `@aws-sdk`); tested against local S3 semantics.
Live AWS/R2 connectivity depends on your credentials + bucket policy. Live AWS/R2 connectivity depends on your credentials + bucket policy.
- v1 buffers each file in memory up to its size cap (fine for images/docs up to tens of MB). - v1 buffers each file in memory up to its size cap (fine for images/docs up to tens of MB).
## Helper and component kit
Use `formatFileSize`, `uploadAccept`, `uploadedFileMap`, `uploaderAttributes`, and `assertUploadedFiles` to keep upload forms and server validation consistent.
Enable `uploaderPlugin()` for:
- `<UploadDropzone />`
- `<UploadStatus />`
The complete blocks compose `Card`, `Alert`, and `Badge` from `@wrnexus/ui`; the specialized upload runtime remains responsible for the native file input and secure transport behavior.
Large files can use `createResumableUploadManager`. Sessions are bounded and
expiring; chunks may arrive out of order, carry SHA-256 checksums, and are
idempotent when retried. Conflicting retries reject, and the object is assembled
only after every exact-sized chunk is present.
```ts
const uploads = createResumableUploadManager({
driver: getStore("documents").driver,
sessions: redisUploadSessionStore,
chunkSize: 5 * 1024 * 1024,
maxBytes: 500 * 1024 * 1024,
accept: ["application/pdf"],
});
const session = await uploads.create({ name: "report.pdf", size, type });
await uploads.uploadChunk(session.id, index, bytes, sha256);
```
The included memory session store is intended for one-process apps and tests.
Multi-instance production deployments should implement `ResumableSessionStore`
with shared durable storage and atomic session updates, and periodically call
`prune()` for abandoned uploads.
+37 -5
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.", "description": "Secure upload drivers, policies, client runtime, helper functions, and reusable upload components.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/uploader"
},
"homepage": "https://wrnexusjs.dev/packages/uploader",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"uploader"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,12 +34,28 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
} },
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./components/*": "./components/*"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.7.0" "@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
},
"wrnexus": {
"plugin": {
"plugin": "./dist/plugin.js",
"export": "default",
"factory": true
}
}, },
"files": [ "files": [
"dist" "dist",
"README.md",
"components"
] ]
} }
+77
View File
@@ -4,6 +4,31 @@
Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework. Part of the **WrNexus** framework — an SSR-first, Bun-native full-stack web framework.
## Boundary contracts
Use `ContractRegistry` with `defineContract` or `defineEvent` to publish the
same schema descriptors for APIs, actions, webhooks, realtime, queues, cron,
pub/sub, plugins, configuration, and environment variables.
```ts
import { ContractRegistry, defineEvent, v } from "@wrnexus/validation";
export const contracts = new ContractRegistry().register(
defineEvent({
name: "user.created",
version: 1,
consumers: ["notification-worker", "audit-service"],
payload: v.object({ userId: v.string().uuid(), createdAt: v.string().date() }),
}),
);
```
Export the registry from `app/contracts.ts`, then accept a baseline with
`wrnexus contracts snapshot`. CI can run `wrnexus contracts check`; removed
contracts/fields, required-field additions, type changes, narrowed enums, and
tighter validation fail with stable `WRN-CONTRACT-*` diagnostics and list known
consumers. A generated `wrnexus.contracts.json` can be used instead of a module.
## Overview ## Overview
Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wire schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`. Define a schema once with the fluent `v` builder, then reuse it in three places: `.parse()` runs server-side and returns coerced values plus per-field errors; `.describe()` emits a plain-JSON `SchemaDescriptor` that the browser runtime interprets (no `eval`, no bundled validator); and helpers like `parseBody` and `parseEnv` wire schemas straight into API routes and startup config. The server rule logic (`applyRule`/`checkField`) and the client runtime (`VALIDATE_RUNTIME`) mirror each other exactly, so a form validates identically in both places. Schemas are conventionally kept in `app/schemas/`.
@@ -178,3 +203,55 @@ const head = `<script>${renderSchemasScript({ signup: signupSchema.describe() })
- Refinements (`refine`) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same `RuleDescriptor` list. - Refinements (`refine`) run only server-side and are never serialized — client and server agree on every other rule because both interpret the same `RuleDescriptor` list.
- No runtime dependencies. Ships as TypeScript source (`src/index.ts`) executed directly by Bun. - No runtime dependencies. Ships as TypeScript source (`src/index.ts`) executed directly by Bun.
- Pairs with the WrNexus server (`@wrnexus/core`) for route handlers and the SSR layer that injects `renderSchemasScript` / `VALIDATE_RUNTIME`. - Pairs with the WrNexus server (`@wrnexus/core`) for route handlers and the SSR layer that injects `renderSchemasScript` / `VALIDATE_RUNTIME`.
## Helper and component kit
The public helper API includes `parseOrThrow`, `ValidationError`, `validationResponse`, `firstValidationError`, `validationSummary`, and `schemaFieldNames`.
Schema output is inferred automatically by `ObjectSchema`, `parseOrThrow`, `parseBody`, `parseEnv`, and `asyncSchema`. Use `InferSchema<typeof schema>` when a named output type is useful:
```ts
const accountSchema = v.object({
email: v.string().email(),
attempts: v.number().integer(),
});
type AccountInput = InferSchema<typeof accountSchema>;
const account = parseOrThrow(accountSchema, input);
// account.email: string
// account.attempts: number
```
Enable `validationPlugin()` for:
- `<ValidationSummary />`
- `<FieldError />`
The summary block composes `Alert` from `@wrnexus/ui`, while `FieldError` remains a lightweight accessible field-level primitive.
Schemas can drive external contracts without maintaining a second definition:
```ts
import {
localizeDescriptor,
openApiRequestBody,
parseDescriptor,
toJsonSchema,
} from "@wrnexus/validation";
const jsonSchema = toJsonSchema(contactSchema, {
id: "urn:example:contact",
title: "Contact request",
});
const requestBody = openApiRequestBody(contactSchema);
const mr = localizeDescriptor(contactSchema, (key, params) =>
translations.t(`validation.${key}`, params),
);
const result = parseDescriptor(mr, input);
```
JSON Schema output targets draft 2020-12, closes unknown object properties, and
maps lengths/ranges/formats/enums/patterns/integer rules. OpenAPI request bodies
reuse the same properties. Localized descriptors preserve explicit custom
messages and fill default required, type-coercion, and rule messages; the same
descriptor is consumable by server parsing and the eval-free browser runtime.
+38 -3
View File
@@ -1,9 +1,25 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.7.0", "version": "0.8.5",
"type": "module", "type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.", "description": "Shared server/browser schemas, validation helpers, form runtime, and reusable error components.",
"license": "MIT", "license": "MIT",
"repository": {
"type": "git",
"url": "https://git.workroot.in/WorkRoot/WRNexusJS.git",
"directory": "packages/validation"
},
"homepage": "https://wrnexusjs.dev/packages/validation",
"bugs": {
"url": "https://git.workroot.in/WorkRoot/WRNexusJS/issues"
},
"keywords": [
"wrnexus",
"bun",
"typescript",
"validation"
],
"sideEffects": false,
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
@@ -18,9 +34,28 @@
".": { ".": {
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"import": "./dist/index.js" "import": "./dist/index.js"
},
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./components/*": "./components/*"
},
"dependencies": {
"@wrnexus/core": "^0.8.5",
"@wrnexus/plugin": "^0.8.5",
"@wrnexus/ui": "^0.8.5"
},
"wrnexus": {
"plugin": {
"plugin": "./dist/plugin.js",
"export": "default",
"factory": true
} }
}, },
"files": [ "files": [
"dist" "dist",
"README.md",
"components"
] ]
} }
-109
View File
@@ -1,109 +0,0 @@
param(
[string]$Root = "E:\WireJS",
[switch]$SkipValidation
)
$ErrorActionPreference = "Stop"
$PackageRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$Root = [System.IO.Path]::GetFullPath($Root)
if (-not (Test-Path (Join-Path $Root "packages\ui\components"))) {
throw "WRNexusJS repository was not found at: $Root"
}
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$backupRoot = Join-Path $Root ".wrnexus\component-showcase-backup-$timestamp"
New-Item -ItemType Directory -Path $backupRoot -Force | Out-Null
$files = @(
"scripts/generate-ui-component-reference.mjs",
"packages/ui/component-catalog.json",
"examples/component-showcase/scripts/generate-showcase.mjs",
"examples/component-showcase/scripts/showcase-profiles.mjs",
"examples/component-showcase/public/playground.js",
"examples/component-showcase/app/styles/global.css",
"examples/component-showcase/test/showcase.test.ts",
"examples/component-showcase/README.md"
)
$generatedFiles = @(
"packages/ui/component-reference.json",
"packages/ui/COMPONENTS.md",
"examples/component-showcase/showcase-manifest.json"
)
function Convert-RelativePath([string]$relative) {
return $relative.Replace("/", [System.IO.Path]::DirectorySeparatorChar)
}
function Backup-Path([string]$relative) {
$platformPath = Convert-RelativePath $relative
$source = Join-Path $Root $platformPath
if (-not (Test-Path $source)) { return }
$destination = Join-Path $backupRoot $platformPath
$destinationParent = Split-Path -Parent $destination
New-Item -ItemType Directory -Path $destinationParent -Force | Out-Null
Copy-Item $source $destination -Recurse -Force
}
foreach ($relative in $files) { Backup-Path $relative }
foreach ($relative in $generatedFiles) { Backup-Path $relative }
Backup-Path "examples/component-showcase/app/pages"
Backup-Path "examples/component-showcase/app/layouts"
foreach ($relative in $files) {
$platformPath = Convert-RelativePath $relative
$source = Join-Path $PackageRoot $platformPath
$destination = Join-Path $Root $platformPath
if (-not (Test-Path $source)) {
throw "Patch file is missing: $source"
}
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
Copy-Item $source $destination -Force
Write-Host "Updated $relative" -ForegroundColor Green
}
function Invoke-BunStep {
param(
[string]$Label,
[string[]]$Arguments
)
Write-Host "`n$Label" -ForegroundColor Cyan
& bun @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Label failed with exit code $LASTEXITCODE."
}
}
Push-Location $Root
try {
Invoke-BunStep "Generating the current UI component reference" @(
"run",
"scripts/generate-ui-component-reference.mjs"
)
$showcaseRoot = Join-Path $Root "examples\component-showcase"
Remove-Item (Join-Path $showcaseRoot ".wrnexus") -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item (Join-Path $showcaseRoot "dist") -Recurse -Force -ErrorAction SilentlyContinue
Push-Location $showcaseRoot
try {
Invoke-BunStep "Generating component showcase pages and manifest" @("run", "generate")
if (-not $SkipValidation) {
Invoke-BunStep "Validating the complete component showcase" @("run", "check")
}
}
finally {
Pop-Location
}
}
finally {
Pop-Location
}
Write-Host "`nComponent showcase update applied successfully." -ForegroundColor Green
Write-Host "Backup: $backupRoot"
if ($SkipValidation) {
Write-Host "Validation was skipped. Run: bun run --cwd examples/component-showcase check" -ForegroundColor Yellow
}
-36
View File
@@ -1,36 +0,0 @@
param(
[Parameter(Mandatory = $false)]
[string]$Root = "E:\WireJS"
)
$ErrorActionPreference = "Stop"
$Root = (Resolve-Path $Root).Path
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Write-Host "Stopping Bun processes..." -ForegroundColor Cyan
Get-Process bun -ErrorAction SilentlyContinue | Stop-Process -Force
Write-Host "Applying overlay reactivity fix..." -ForegroundColor Cyan
node (Join-Path $ScriptDir "patch-overlay-showcase.mjs") $Root
Push-Location $Root
try {
Write-Host "Regenerating UI component reference..." -ForegroundColor Cyan
bun run scripts/generate-ui-component-reference.mjs
Write-Host "Running focused UI tests..." -ForegroundColor Cyan
bun test packages/ui/test/overlay-reactivity.test.ts
Push-Location (Join-Path $Root "examples\component-showcase")
try {
Write-Host "Regenerating showcase..." -ForegroundColor Cyan
bun run generate
bun test test/showcase.test.ts
} finally {
Pop-Location
}
} finally {
Pop-Location
}
Write-Host "Done. Start with: cd E:\WireJS\examples\component-showcase; bun run dev" -ForegroundColor Green
-16
View File
@@ -1,16 +0,0 @@
{
"name": "WRNexusJS",
"version": "0.6.0",
"status": "developer-test-build",
"sourceArchive": "WRNexusJS(4).zip",
"frameworkPackages": 33,
"uiComponents": 108,
"showcaseComponentPages": 108,
"showcaseLiveDemos": 417,
"showcaseComponentCategories": 11,
"showcaseV06Guides": 8,
"wrnFilesParsed": 250,
"wrnParseFailures": 0,
"bunFullSuiteExecuted": false,
"bunUnavailableInPackagingEnvironment": true
}
+50
View File
@@ -0,0 +1,50 @@
# Changelog
All notable framework changes are recorded here. Every release must also include an idempotent
entry in the CLI migration registry.
## 0.8.3 - 2026-08-03
- Bundled hydrated browser modules so package aliases and filesystem imports do not leak into
browser-native modules.
- Fixed SSR computed values, Async branch aliases, typed object props in loops, reactive route
state, loader invalidation, realtime identity isolation, and client-function hydration.
- Fixed template-literal parsing in the reactive fallback evaluator and removed the unsupported
default `unload` Permissions Policy directive.
- Made official package components compatible with strict explicit imports and added regression
coverage for the TeamSpace application failures.
- Fixed Bun 1.3.14 and TypeScript 5.9 release-gate compatibility in client bundling, alias
resolution, and Happy DOM event tests.
- Fixed ESLint compatibility in Node release scripts by declaring Node globals and using explicit
regex space quantifiers.
- Fixed package component import validation after workspace installation by excluding generated and
dependency directories such as nested `node_modules` from first-party source scans.
- Made the VS Code embedded compiler freshness check deterministic across TypeScript patch
versions, Windows/Linux line endings, and workspace environments by verifying normalized source
and generator fingerprints while retaining exact-output checks for the same TypeScript version.
- Fixed the VS Code language-server bundle so it executes under Node instead of exiting after
defining an uninvoked Bun CommonJS wrapper, and added request-level crash containment.
- Scoped HTML diagnostics to `view` blocks, ignored TypeScript generic syntax and WRN expressions,
and preserved JavaScript-looking documentation inside `<pre><code>` examples.
- Fixed formatter corruption of preformatted examples, balanced compact sibling markup, long bare
tags, and repeated format-on-save indentation drift.
- Fixed component prop intelligence for dynamic boolean/union expressions, boolean string literals,
literal-union runtime types, and reserved prop names such as `class`.
- Prevented bundled editor type checking from publishing TypeScript standard-library path failures or
unmapped synthetic virtual-document diagnostics.
- Fixed language-server virtual TypeScript inference for untyped dynamic handler parameters and
indexed output dispatch, while retaining strict diagnostics for explicitly typed parameters.
- Made bundled editor type checking resolve TypeScript standard libraries from the active workspace,
and added a regression test that proves semantic diagnostics are actually running.
- Fixed final release-gate lint failures by removing the obsolete editor `stripComments` helper and
importing Node `Buffer` explicitly in the language-server bundle generator.
- Reviewed the UI visual-contract change caused only by corrected `AuthForm.wrn` indentation and
regenerated the 0.8 baseline after confirming no rendered component behavior changed.
## 0.8.0 - 2026-08-02
- Added package-owned helper and component kits across all 39 framework packages.
- Added standalone realtime and package-aware auth, i18n, image, uploader, validation, JWT,
encryption, database, and CAPTCHA improvements.
- Added whole-application WRN syntax, import, and formatting modernization to the CLI update.
- Added Windows/Linux CI, read-only package audits, governance documents, and security gates.
-421
View File
@@ -1,421 +0,0 @@
## R9 — ToggleCount animation stability
- Schedules animation frames by absolute offsets to prevent cumulative timer drift.
- Cancels stale ToggleCount animation frames with per-animation tokens.
- Binds generated browser peer functions through a scoped function table.
# WRNexusJS v0.6.0 changed files
- Canonicalized v0.6 syntax helpers through `@wrnexus/syntax`; removed the public `@wrnexus/syntax/v060` subpath and editor-bundle alias.
## R3 correction
- Regenerated/aligned `editors/vscode/src/compiler.cjs`.
- Added `FIXES-0.6.0-VSCODE-COMPILER-BUNDLE.md`.
Comparison base: `WRNexusJS(4).zip`. Dependency directories, `.git`, and `.wrnexus` caches are excluded from this comparison.
- Added: **57** files
- Modified: **284** files
- Removed: **0** files
## Added files
- `MIGRATION-DRY-RUN-0.6.0.json`
- `MIGRATION-DRY-RUN-0.6.0.md`
- `OLD-PROJECT-TEST-CHECKLIST.md`
- `PUBLISHING-0.6.0.md`
- `RELEASE_MANIFEST.json`
- `RELEASE_NOTES-0.6.0.md`
- `ROLLBACK-0.6.0.md`
- `UPGRADE-0.6.0.md`
- `VALIDATION-0.6.0.md`
- `docs/v0.6/architecture.md`
- `docs/v0.6/language.md`
- `examples/component-showcase/app/pages/v06-functions.wrn`
- `examples/component-showcase/app/pages/v06-imports.wrn`
- `examples/component-showcase/app/pages/v06-migration.wrn`
- `examples/component-showcase/app/pages/v06-outputs.wrn`
- `examples/component-showcase/app/pages/v06-runtime.wrn`
- `examples/component-showcase/app/pages/v06-state.wrn`
- `examples/component-showcase/app/pages/v06-stores.wrn`
- `examples/component-showcase/app/pages/v06-types.wrn`
- `integration/fixtures/v0.5-legacy-app/app/components/LegacyModal.wrn`
- `integration/fixtures/v0.5-legacy-app/app/layouts/PublicLayout.wrn`
- `integration/fixtures/v0.5-legacy-app/app/pages/index.wrn`
- `integration/fixtures/v0.5-legacy-app/app/types/global.d.ts`
- `integration/fixtures/v0.5-legacy-app/package.json`
- `packages/cli/test/update-v060.test.ts`
- `packages/compiler/src/client-codegen.ts`
- `packages/compiler/src/component-contract.ts`
- `packages/compiler/src/import-resolver.ts`
- `packages/compiler/src/server-codegen.ts`
- `packages/compiler/src/source-map.ts`
- `packages/compiler/src/store-codegen.ts`
- `packages/compiler/src/targets.ts`
- `packages/compiler/src/type-codegen.ts`
- `packages/compiler/test/v060-targets.test.ts`
- `packages/csr/src/client-functions.ts`
- `packages/csr/src/outputs.ts`
- `packages/csr/src/refs.ts`
- `packages/csr/src/server-client.ts`
- `packages/csr/src/types.ts`
- `packages/ssr/src/rpc.ts`
- `packages/ssr/src/store-context.ts`
- `packages/ssr/test/rpc-v060.test.ts`
- `packages/store/package.json`
- `packages/store/src/client.ts`
- `packages/store/src/index.ts`
- `packages/store/src/server.ts`
- `packages/store/src/types.ts`
- `packages/store/test/store.test.ts`
- `packages/syntax/src/v060.ts`
- `packages/syntax/test/v060.test.ts`
- `packages/typecheck/package.json`
- `packages/typecheck/src/contracts.ts`
- `packages/typecheck/src/index.ts`
- `packages/typecheck/src/project.ts`
- `packages/typecheck/test/imports-and-components.test.ts`
- `packages/typecheck/test/typecheck.test.ts`
- `scripts/validate-0.6.mjs`
## Modified files
- `bun.lock`
- `editors/vscode/package.json`
- `editors/vscode/snippets/wrn.json`
- `editors/vscode/src/completion.js`
- `editors/vscode/src/diagnostics.js`
- `editors/vscode/syntaxes/wrn.tmLanguage.json`
- `examples/component-showcase/app/layouts/showcase.wrn`
- `examples/component-showcase/app/pages/components/accordion.wrn`
- `examples/component-showcase/app/pages/components/advanced-date-picker.wrn`
- `examples/component-showcase/app/pages/components/advanced-range-slider.wrn`
- `examples/component-showcase/app/pages/components/advanced-select.wrn`
- `examples/component-showcase/app/pages/components/alert.wrn`
- `examples/component-showcase/app/pages/components/announcement-bar.wrn`
- `examples/component-showcase/app/pages/components/auth-form.wrn`
- `examples/component-showcase/app/pages/components/auth-split-layout.wrn`
- `examples/component-showcase/app/pages/components/avatar-group.wrn`
- `examples/component-showcase/app/pages/components/avatar.wrn`
- `examples/component-showcase/app/pages/components/back-to-top.wrn`
- `examples/component-showcase/app/pages/components/badge.wrn`
- `examples/component-showcase/app/pages/components/blockquote.wrn`
- `examples/component-showcase/app/pages/components/breadcrumb.wrn`
- `examples/component-showcase/app/pages/components/button-group.wrn`
- `examples/component-showcase/app/pages/components/button.wrn`
- `examples/component-showcase/app/pages/components/card.wrn`
- `examples/component-showcase/app/pages/components/carousel.wrn`
- `examples/component-showcase/app/pages/components/chart.wrn`
- `examples/component-showcase/app/pages/components/chat-bubble.wrn`
- `examples/component-showcase/app/pages/components/checkbox.wrn`
- `examples/component-showcase/app/pages/components/clipboard.wrn`
- `examples/component-showcase/app/pages/components/collapse.wrn`
- `examples/component-showcase/app/pages/components/color-picker.wrn`
- `examples/component-showcase/app/pages/components/columns.wrn`
- `examples/component-showcase/app/pages/components/combo-box.wrn`
- `examples/component-showcase/app/pages/components/confetti.wrn`
- `examples/component-showcase/app/pages/components/container.wrn`
- `examples/component-showcase/app/pages/components/context-menu.wrn`
- `examples/component-showcase/app/pages/components/copy-markup.wrn`
- `examples/component-showcase/app/pages/components/ctasection.wrn`
- `examples/component-showcase/app/pages/components/custom-scrollbar.wrn`
- `examples/component-showcase/app/pages/components/data-map.wrn`
- `examples/component-showcase/app/pages/components/data-table.wrn`
- `examples/component-showcase/app/pages/components/date-picker.wrn`
- `examples/component-showcase/app/pages/components/device-frame.wrn`
- `examples/component-showcase/app/pages/components/divider.wrn`
- `examples/component-showcase/app/pages/components/drag-and-drop.wrn`
- `examples/component-showcase/app/pages/components/drawer.wrn`
- `examples/component-showcase/app/pages/components/dropdown.wrn`
- `examples/component-showcase/app/pages/components/feature-card.wrn`
- `examples/component-showcase/app/pages/components/feature-grid.wrn`
- `examples/component-showcase/app/pages/components/feature-icon-card.wrn`
- `examples/component-showcase/app/pages/components/file-input.wrn`
- `examples/component-showcase/app/pages/components/file-upload-progress.wrn`
- `examples/component-showcase/app/pages/components/file-upload.wrn`
- `examples/component-showcase/app/pages/components/footer.wrn`
- `examples/component-showcase/app/pages/components/grid.wrn`
- `examples/component-showcase/app/pages/components/hero-actions.wrn`
- `examples/component-showcase/app/pages/components/hero.wrn`
- `examples/component-showcase/app/pages/components/image.wrn`
- `examples/component-showcase/app/pages/components/input-group.wrn`
- `examples/component-showcase/app/pages/components/input-number.wrn`
- `examples/component-showcase/app/pages/components/input.wrn`
- `examples/component-showcase/app/pages/components/kbd.wrn`
- `examples/component-showcase/app/pages/components/layout-splitter.wrn`
- `examples/component-showcase/app/pages/components/legend-indicator.wrn`
- `examples/component-showcase/app/pages/components/link.wrn`
- `examples/component-showcase/app/pages/components/list-group.wrn`
- `examples/component-showcase/app/pages/components/list.wrn`
- `examples/component-showcase/app/pages/components/map.wrn`
- `examples/component-showcase/app/pages/components/marketing-section-header.wrn`
- `examples/component-showcase/app/pages/components/marquee.wrn`
- `examples/component-showcase/app/pages/components/mega-menu.wrn`
- `examples/component-showcase/app/pages/components/metric-card.wrn`
- `examples/component-showcase/app/pages/components/metric-grid.wrn`
- `examples/component-showcase/app/pages/components/modal.wrn`
- `examples/component-showcase/app/pages/components/nav.wrn`
- `examples/component-showcase/app/pages/components/navbar.wrn`
- `examples/component-showcase/app/pages/components/page-header.wrn`
- `examples/component-showcase/app/pages/components/pagination.wrn`
- `examples/component-showcase/app/pages/components/pin-input.wrn`
- `examples/component-showcase/app/pages/components/popover.wrn`
- `examples/component-showcase/app/pages/components/portal-dashboard.wrn`
- `examples/component-showcase/app/pages/components/preference-switcher.wrn`
- `examples/component-showcase/app/pages/components/progress.wrn`
- `examples/component-showcase/app/pages/components/public-page-shell.wrn`
- `examples/component-showcase/app/pages/components/radio.wrn`
- `examples/component-showcase/app/pages/components/range-slider.wrn`
- `examples/component-showcase/app/pages/components/rating.wrn`
- `examples/component-showcase/app/pages/components/scrollspy.wrn`
- `examples/component-showcase/app/pages/components/search-box.wrn`
- `examples/component-showcase/app/pages/components/section-header.wrn`
- `examples/component-showcase/app/pages/components/section.wrn`
- `examples/component-showcase/app/pages/components/select.wrn`
- `examples/component-showcase/app/pages/components/sidebar.wrn`
- `examples/component-showcase/app/pages/components/skeleton.wrn`
- `examples/component-showcase/app/pages/components/spinner.wrn`
- `examples/component-showcase/app/pages/components/split-hero.wrn`
- `examples/component-showcase/app/pages/components/stats-bar.wrn`
- `examples/component-showcase/app/pages/components/stepper.wrn`
- `examples/component-showcase/app/pages/components/strong-password.wrn`
- `examples/component-showcase/app/pages/components/styled-icon.wrn`
- `examples/component-showcase/app/pages/components/switch.wrn`
- `examples/component-showcase/app/pages/components/table.wrn`
- `examples/component-showcase/app/pages/components/tabs.wrn`
- `examples/component-showcase/app/pages/components/text-link.wrn`
- `examples/component-showcase/app/pages/components/textarea.wrn`
- `examples/component-showcase/app/pages/components/time-picker.wrn`
- `examples/component-showcase/app/pages/components/timeline.wrn`
- `examples/component-showcase/app/pages/components/toast-notifications.wrn`
- `examples/component-showcase/app/pages/components/toast.wrn`
- `examples/component-showcase/app/pages/components/toggle-count.wrn`
- `examples/component-showcase/app/pages/components/toggle-password.wrn`
- `examples/component-showcase/app/pages/components/tooltip.wrn`
- `examples/component-showcase/app/pages/components/tree-view.wrn`
- `examples/component-showcase/app/pages/components/typography.wrn`
- `examples/component-showcase/app/pages/components/wysiwyg-editor.wrn`
- `examples/component-showcase/public/playground.js`
- `examples/component-showcase/scripts/generate-showcase.mjs`
- `llms.txt`
- `package.json`
- `packages/ai/package.json`
- `packages/auth/package.json`
- `packages/authz/package.json`
- `packages/captcha/package.json`
- `packages/cli/package.json`
- `packages/cli/src/update.ts`
- `packages/compiler/package.json`
- `packages/compiler/src/codegen.ts`
- `packages/compiler/src/index.ts`
- `packages/core/package.json`
- `packages/csr/package.json`
- `packages/csr/src/index.ts`
- `packages/csr/src/nav-runtime.ts`
- `packages/csr/src/reactive-runtime.ts`
- `packages/db/package.json`
- `packages/dev-server/package.json`
- `packages/dev-server/src/assets.ts`
- `packages/dev-server/src/index.ts`
- `packages/dev-server/src/pipeline.ts`
- `packages/dev-server/src/runtime.ts`
- `packages/dev-toolbar/package.json`
- `packages/dev-toolbar/src/client/runtime.ts`
- `packages/dev-toolbar/src/client/styles.ts`
- `packages/dev-toolbar/src/types.ts`
- `packages/encryption/package.json`
- `packages/helpers/package.json`
- `packages/i18n/package.json`
- `packages/jwt/package.json`
- `packages/mobile/package.json`
- `packages/native/package.json`
- `packages/oauth/package.json`
- `packages/plugin/package.json`
- `packages/pubsub/package.json`
- `packages/queue/package.json`
- `packages/reactive/package.json`
- `packages/router/package.json`
- `packages/ssr/package.json`
- `packages/ssr/src/index.ts`
- `packages/styles/package.json`
- `packages/styles/src/config.ts`
- `packages/syntax/package.json`
- `packages/syntax/src/diagnostics.ts`
- `packages/syntax/src/index.ts`
- `packages/syntax/src/parser.ts`
- `packages/syntax/src/spec.ts`
- `packages/syntax/src/tokenizer.ts`
- `packages/test/package.json`
- `packages/tracking/package.json`
- `packages/ui/COMPONENTS.md`
- `packages/ui/component-reference.json`
- `packages/ui/components/Accordion.wrn`
- `packages/ui/components/AdvancedDatePicker.wrn`
- `packages/ui/components/AdvancedRangeSlider.wrn`
- `packages/ui/components/AdvancedSelect.wrn`
- `packages/ui/components/AnnouncementBar.wrn`
- `packages/ui/components/AuthForm.wrn`
- `packages/ui/components/AuthSplitLayout.wrn`
- `packages/ui/components/AvatarGroup.wrn`
- `packages/ui/components/BackToTop.wrn`
- `packages/ui/components/Badge.wrn`
- `packages/ui/components/Blockquote.wrn`
- `packages/ui/components/Breadcrumb.wrn`
- `packages/ui/components/ButtonGroup.wrn`
- `packages/ui/components/CTASection.wrn`
- `packages/ui/components/Card.wrn`
- `packages/ui/components/Chart.wrn`
- `packages/ui/components/ChatBubble.wrn`
- `packages/ui/components/Checkbox.wrn`
- `packages/ui/components/Clipboard.wrn`
- `packages/ui/components/Collapse.wrn`
- `packages/ui/components/ColorPicker.wrn`
- `packages/ui/components/Columns.wrn`
- `packages/ui/components/Combobox.wrn`
- `packages/ui/components/Confetti.wrn`
- `packages/ui/components/Container.wrn`
- `packages/ui/components/ContextMenu.wrn`
- `packages/ui/components/CopyMarkup.wrn`
- `packages/ui/components/CustomScrollbar.wrn`
- `packages/ui/components/DataMap.wrn`
- `packages/ui/components/DataTable.wrn`
- `packages/ui/components/DatePicker.wrn`
- `packages/ui/components/DeviceFrame.wrn`
- `packages/ui/components/Divider.wrn`
- `packages/ui/components/DragAndDrop.wrn`
- `packages/ui/components/Drawer.wrn`
- `packages/ui/components/Dropdown.wrn`
- `packages/ui/components/FeatureCard.wrn`
- `packages/ui/components/FeatureGrid.wrn`
- `packages/ui/components/FeatureIconCard.wrn`
- `packages/ui/components/FileInput.wrn`
- `packages/ui/components/FileUpload.wrn`
- `packages/ui/components/FileUploadProgress.wrn`
- `packages/ui/components/Footer.wrn`
- `packages/ui/components/Grid.wrn`
- `packages/ui/components/Hero.wrn`
- `packages/ui/components/HeroActions.wrn`
- `packages/ui/components/Image.wrn`
- `packages/ui/components/Input.wrn`
- `packages/ui/components/InputGroup.wrn`
- `packages/ui/components/InputNumber.wrn`
- `packages/ui/components/Kbd.wrn`
- `packages/ui/components/LayoutSplitter.wrn`
- `packages/ui/components/LegendIndicator.wrn`
- `packages/ui/components/Link.wrn`
- `packages/ui/components/List.wrn`
- `packages/ui/components/ListGroup.wrn`
- `packages/ui/components/Map.wrn`
- `packages/ui/components/MarketingSectionHeader.wrn`
- `packages/ui/components/Marquee.wrn`
- `packages/ui/components/MegaMenu.wrn`
- `packages/ui/components/MetricCard.wrn`
- `packages/ui/components/MetricGrid.wrn`
- `packages/ui/components/Modal.wrn`
- `packages/ui/components/Nav.wrn`
- `packages/ui/components/Navbar.wrn`
- `packages/ui/components/PageHeader.wrn`
- `packages/ui/components/Pagination.wrn`
- `packages/ui/components/PinInput.wrn`
- `packages/ui/components/Popover.wrn`
- `packages/ui/components/PortalDashboard.wrn`
- `packages/ui/components/PreferenceSwitcher.wrn`
- `packages/ui/components/PublicPageShell.wrn`
- `packages/ui/components/Radio.wrn`
- `packages/ui/components/RangeSlider.wrn`
- `packages/ui/components/Rating.wrn`
- `packages/ui/components/Scrollspy.wrn`
- `packages/ui/components/SearchBox.wrn`
- `packages/ui/components/Section.wrn`
- `packages/ui/components/SectionHeader.wrn`
- `packages/ui/components/Select.wrn`
- `packages/ui/components/Sidebar.wrn`
- `packages/ui/components/SplitHero.wrn`
- `packages/ui/components/StatsBar.wrn`
- `packages/ui/components/Stepper.wrn`
- `packages/ui/components/StrongPassword.wrn`
- `packages/ui/components/StyledIcon.wrn`
- `packages/ui/components/Switch.wrn`
- `packages/ui/components/Tabs.wrn`
- `packages/ui/components/TextLink.wrn`
- `packages/ui/components/Textarea.wrn`
- `packages/ui/components/TimePicker.wrn`
- `packages/ui/components/Timeline.wrn`
- `packages/ui/components/Toast.wrn`
- `packages/ui/components/ToastNotifications.wrn`
- `packages/ui/components/ToggleCount.wrn`
- `packages/ui/components/TogglePassword.wrn`
- `packages/ui/components/Tooltip.wrn`
- `packages/ui/components/TreeView.wrn`
- `packages/ui/components/Typography.wrn`
- `packages/ui/components/WysiwygEditor.wrn`
- `packages/ui/components/alert.wrn`
- `packages/ui/components/avatar.wrn`
- `packages/ui/components/button.wrn`
- `packages/ui/components/carousel.wrn`
- `packages/ui/components/progress.wrn`
- `packages/ui/components/skeleton.wrn`
- `packages/ui/components/spinner.wrn`
- `packages/ui/components/table.wrn`
- `packages/ui/package.json`
- `packages/uploader/package.json`
- `packages/validation/package.json`
- `scripts/generate-ui-component-reference.mjs`
- `services/managed-captcha/package.json`
- `tsconfig.json`
- `update-package-versions.mjs`
## Removed files
- None
## R7 test-cascade corrections
- Restored synchronous CSR hydration for inline behavior and unresolved direct-compile module placeholders.
- Restored strict component/store lifecycle hook validation.
- Prevented browser runtime API names from colliding with generated prop/state aliases.
- Preserved synchronous page exports when no imported store requires asynchronous initialization.
- Allowed store dispose lifecycle hooks to mutate store state through the internal mutation context.
- Updated UI tests and generated references for typed props and `outputs {}` contracts.
- Added universal size/color support to FeatureGrid and complete RangeSlider output metadata.
## R8 UI compiler regression fixes
- Fixed readonly-prop false positives for comparisons, string literals, object/member access, and shadowing function parameters.
- Restored compilation and runtime tests for AuthForm, Carousel, RangeSlider, InputNumber, Select, AdvancedSelect, ComboBox, ContextMenu, and Tooltip.
- Synchronized component catalog and generated component reference with all 108 bundled UI declarations.
- Added full UI compilation and native-attribute forwarding checks to `validate:0.6`.
## R10 peer-function scoped-state correction
- Peer client/shared functions are invoked through synchronized scoped wrappers.
- Nested peer calls no longer have their state changes overwritten by stale outer aliases.
- Synchronous, asynchronous, and throwing peer calls refresh shared state correctly.
- Added an executable `validate:0.6` regression probe for the exact compiler failure.
## R11 peer-function test correction
- Corrected the compiler regression test export-stripping regex from an over-escaped literal `\\s` match to the intended whitespace `\s` match.
- Confirmed the generated peer-function browser module executes and updates shared state from `0` to `1`.
- Added a release validator guard for the exact test source regression.
## R13 release reference stability
- Prevented the UI reference generator from rewriting current files solely because of CRLF/LF differences.
- Added an executable regression test proving line-ending-only differences are ignored and real stale content is repaired.
## 0.7.0 R2 typecheck correction
- Removed leaked focused-typecheck shim files that polluted the root TypeScript program.
- Added a validation gate preventing those temporary files from entering future archives.
## 0.7.0 R4 Happy DOM event typing
- Fixed strict event-type compatibility in `packages/validation/test/validation.test.ts`.
- Improved `check:workspace` dry-run wording.
- Added a validation guard preventing browser DOM `Event` from being reintroduced as the Happy DOM helper return type.
## 0.7.0 R6 lint cleanup
See `FIXES-0.7.0-R6-LINT-CLEANUP.md` for the final lint and secure hydration corrections.
+8
View File
@@ -0,0 +1,8 @@
# Code of conduct
Be respectful, constructive, and specific. Harassment, discrimination, threats, personal attacks,
and publication of private information are not acceptable. Discuss technical decisions with
evidence, assume good intent, and give contributors room to correct mistakes.
Report conduct concerns privately to the maintainers. Maintainers may remove content, limit
participation, or ban contributors when necessary to protect the community.
+17
View File
@@ -0,0 +1,17 @@
# Contributing
Install Bun 1.3.14 or newer and Node.js 24, then run:
```sh
bun install --frozen-lockfile
npm ci --prefix editors/vscode
bun run typecheck
bun run lint
bun run format:check
bun run test:all
bun run validate:0.8
```
Keep migrations conservative, backed up, idempotent, and covered by fixtures. Generated files
must be produced by their documented `generate:*` command and committed with their source change.
Security issues follow `SECURITY.md` and must not be disclosed publicly before a coordinated fix.
-65
View File
@@ -1,65 +0,0 @@
# WRNexusJS 0.6.0 focused fixes — issues 1 through 7
This developer-test archive fixes the seven blockers identified in `WRNexusJS-v0.6.0-validation-against-spec.md`.
## Fixed
1. **Generated browser-module syntax**
- JavaScript reserved words such as the `class` prop are never destructured into invalid bindings.
- Function parameters no longer collide with generated prop, state, output, server, props, or refs aliases.
- All 108 UI component browser modules were regenerated and passed JavaScript syntax parsing.
2. **Browser-store RPC**
- Generated store actions bind `server` from the store action context.
- Browser store RPC sends same-origin structured requests with CSRF and request identifiers.
- A live simulated store called a server function successfully and updated reactive state.
3. **Concrete UI output contracts**
- No `unknown` remains inside any UI `outputs {}` declaration.
- Component reference and showcase metadata were regenerated.
- Legacy `$emit`, `@event`, `$event`, and `event.detail` remain absent from framework UI components.
4. **Configured import modes**
- Dev compilation reads `imports.mode`, `imports.aliases`, and `imports.autoImport` from application configuration.
- `legacy` accepts implicit discovery.
- `compatible` accepts it with `WRN-IMPORT-IMPLICIT` diagnostics.
- `explicit` rejects missing imports and accepts correctly imported components/layouts/stores.
5. **Store persistence migration and validation**
- `persist { migrations { ... } validate { ... } }` is parsed and emitted.
- Browser restore executes version migration before validation.
- Invalid or incompatible persisted values are reset with diagnostics.
- Include-only state is written with the current persistence version.
6. **Store HMR integration**
- Store browser modules expose hot-update definitions.
- The dev server broadcasts versioned `store-update` messages.
- The browser HMR client imports changed store modules and applies updates without a mandatory document reload.
- Compatible fields are preserved; added, removed, and incompatible fields are reported.
7. **Restricted RPC exposure**
- RPC manifests include only server functions referenced through `server.name(...)` by browser-capable code.
- Unreferenced server functions remain available for local SSR/server execution but are not remotely exposed.
## Focused validation completed
- 108 UI browser artifacts generated: **0 generation failures**.
- 108 UI browser artifacts syntax checked: **0 syntax failures**.
- 250 UI/showcase `.wrn` files parsed: **0 parser failures**.
- Browser-store RPC/persistence/validation/HMR simulation: **passed**.
- Import mode simulation for legacy/compatible/explicit: **passed**.
- Restricted component and store RPC manifest probes: **passed**.
- Generated server-store TypeScript transpilation: **0 diagnostics**.
- VS Code extension Node tests: **28 passed**.
- `node scripts/validate-0.6.mjs`: **12 passed, 1 Bun warning, 0 failed**.
## Remaining local release gate
Bun is unavailable in the packaging environment. Run the full point-8 suite locally before publishing:
```powershell
bun install
bun run validate:0.6
bun run check
bun run --cwd examples/component-showcase check
```
-44
View File
@@ -1,44 +0,0 @@
# WRNexusJS v0.6.0 R6 lint corrections
This revision fixes the lint failures reported by `bun run check` after R5.
## Corrected files
- `editors/vscode/test/formatter.test.js`
- Replaced hard-to-count literal regex spaces with quantified spaces.
- `examples/component-showcase/scripts/generate-showcase.mjs`
- Removed unused `uiComponentsPath` and `jsonAttribute` declarations.
- `examples/component-showcase/scripts/showcase-profiles.mjs`
- Removed the unused `actionSlot` declaration.
- `packages/cli/src/update.ts`
- Removed an unnecessary regex escape.
- `packages/compiler/src/client-codegen.ts`
- Rewrote store-path regexes without unnecessary character-class escapes.
- `packages/compiler/src/codegen.ts`
- Rewrote the store-path regex without unnecessary character-class escapes.
- `packages/syntax/src/diagnostics.ts`
- Corrected dynamic `RegExp` string escaping for word boundaries, property separators, and whitespace.
- `packages/syntax/src/parser.ts`
- Removed an unused `FunctionRuntime` type import.
- `patch-overlay-showcase.mjs`
- Added explicit Node imports for `process` and `console`.
- `scripts/generate-ui-component-reference.mjs`
- Removed an unnecessary closing-parenthesis escape in a regex character class.
- `scripts/validate-0.6.mjs`
- Added explicit Node imports for `console`, `process`, and `fileURLToPath`.
- Replaced URL pathname manipulation with `fileURLToPath`, improving Windows path handling.
## Validation performed in the packaging environment
- `node scripts/validate-0.6.mjs`: 14 passes, zero failures; Bun unavailable warning only.
- VS Code extension Node tests: 28 passes, zero failures.
- VS Code extension validation: all checks passed.
- All JavaScript and MJS syntax checks passed.
- Exact regression guards for all 31 reported lint findings passed.
Run the complete release gate locally with Bun:
```powershell
bun install
bun run check
```
-25
View File
@@ -1,25 +0,0 @@
# WRNexusJS v0.6.0 R10 peer-function state fix
## Failure
The compiler test `browser codegen binds peer client functions through the scoped function table` failed because an outer client function captured local state before calling a peer function. The peer updated `context.state`, but the outer function's `finally` block copied its stale local value back over the peer update.
## Fix
`packages/compiler/src/client-codegen.ts` now generates peer-function wrappers that:
1. synchronize the caller's local state into `context.state` before the peer call;
2. invoke the peer through `context.functions`;
3. refresh local aliases from `context.state` after synchronous completion, asynchronous completion, or a synchronous throw;
4. copy local state back only when it actually changed from the function-entry snapshot.
This preserves shared scoped state for nested client/shared function calls without changing synchronous functions into promises.
## Regression validation
- Exact peer-call execution probe: passed (`run()` calls `increment()`, final state is `1`).
- Focused TypeScript compiler check: passed.
- UI browser targets generated and syntax-checked: 108/108.
- UI and showcase WRN files parsed: 250/250.
- VS Code tests: 28/28.
- Root v0.6 validator: 18 passed, 0 failed (Bun availability warning only in the packaging environment).
-17
View File
@@ -1,17 +0,0 @@
# WRNexusJS 0.6.0 R11 peer-function regression test fix
The final failing compiler test used an over-escaped regular expression:
```ts
targets.browser.replace(/^export\\s+/gm, "");
```
That pattern matches a literal `\\s` sequence rather than whitespace, so generated `export` keywords remained in the source passed to `new Function()`.
R11 corrects the test to:
```ts
targets.browser.replace(/^export\s+/gm, "");
```
The generated browser module itself was already correct. The corrected executable probe returns `state.value === 1` after calling the peer-bound `run()` function.
-33
View File
@@ -1,33 +0,0 @@
# R12 release reference gate fix
## Failure
`bun run scripts/release.ts publish` could stop with:
```text
UI component reference is stale.
M packages/ui/component-reference.json
```
even when the generated content was current.
## Root cause
The release gate used `git status --short` immediately after the generator ran. On Windows, generated files are written with LF while an `autocrlf` checkout may use CRLF. Git status can report the worktree file as modified even when its canonical Git content is unchanged.
The generator also writes `component-catalog.json`, but that file was not part of the release gate. Component ordering used `localeCompare`, which can vary by operating-system locale.
## Fix
- Compare generated files with `git diff --quiet`, which applies Git text normalization.
- Show real differences with `git diff --name-status`.
- Include all generated UI reference files:
- `packages/ui/component-catalog.json`
- `packages/ui/component-reference.json`
- `packages/ui/COMPONENTS.md`
- Replace locale-dependent sorting with a deterministic ASCII comparator.
- Add validation guards for the complete release gate.
## Expected result
A line-ending-only rewrite does not block publication. A real component-reference, catalog, or documentation difference still blocks publication and prints the exact changed files.
-19
View File
@@ -1,19 +0,0 @@
# WRNexusJS 0.6.0 R13 - UI reference release stability
## Problem
`release:private` regenerated the UI component catalog and reference before publishing. On Windows, the generator always wrote LF output. When the checked-out JSON files used CRLF, Git reported both generated JSON files as modified even though their normalized content was already current.
## Fix
- Added normalized newline comparison to `scripts/generate-ui-component-reference.mjs`.
- Generated files are no longer rewritten when their content differs only by line endings.
- Real generated-content differences are still written and remain visible to the release gate.
- Added `scripts/test-ui-reference-generation.mjs` to execute both the CRLF no-op case and the genuine stale-content repair case.
- Wired the executable regression into `scripts/validate-0.6.mjs`.
## Verified behavior
- Current CRLF catalog/reference files remain byte-for-byte unchanged.
- A stale reference count is regenerated to the correct value.
- Release verification still blocks real generated-content drift.
-21
View File
@@ -1,21 +0,0 @@
# WRNexusJS v0.6.0 R5 store type regression fix
R4 accidentally restored an older `@wrnexus/store` type surface while changing the syntax package import boundary.
## Corrected
- Shared, client, and server store state now use separate generic types.
- `StoreCombinedState<S, CS, SS>` is used by computed values, actions, persistence, and lifecycle hooks.
- Store action inference defaults to callable `StoreFunction` values instead of `never`.
- `createClientState` and `createServerState` no longer have to overlap with shared state.
- The internal initialization promise is named `whenReady`, allowing application state to use `ready` safely.
- Generated browser stores also expose `whenReady` rather than reserving `ready`.
- `validate:0.6` now guards all of these signatures to prevent regression.
## Focused validation
- `packages/store/test/store.test.ts` passes strict TypeScript checking with a local `bun:test` type shim.
- Runtime probe passes client/server action selection, computed values, client-only state, server-state serialization exclusion, and a boolean state field named `ready`.
- VS Code tests: 28 passed.
- VS Code validation: all checks passed.
- Repository v0.6 validator: 14 passed, 0 failed; only the expected local Bun availability warning remains in the packaging environment.
-53
View File
@@ -1,53 +0,0 @@
# WRNexusJS v0.6.0 R7 test-cascade fixes
This revision addresses the 71 failures reported after running `bun run check` on R6.
## Root fixes
1. **Synchronous CSR hydration restored**
- Scopes without a browser module hydrate immediately again.
- The unresolved `__WRNEXUS_CLIENT_MODULE__` placeholder used by direct compiler/test rendering also hydrates inline behavior immediately.
- Real resolved browser modules still load asynchronously before scope setup.
2. **Component event target selection corrected**
- A scope with its own `data-wrn-events` now uses itself before searching descendants.
3. **Lifecycle validation restored**
- Component lifecycle supports `mount`, `update`, and `unmount`.
- Compatibility aliases `clientInit` and `dispose` map to mount/unmount.
- Unknown component and store lifecycle hooks fail parsing.
4. **Browser binding collisions fixed**
- Runtime names such as `output`, `server`, `props`, and `refs` are not generated as implicit state/prop aliases.
- Function parameters may still intentionally use those names.
5. **Synchronous page exports preserved**
- Pages remain synchronous unless imported stores require `await`.
- This restores in-process WRN HMR module behavior.
6. **Store dispose lifecycle mutations fixed**
- `dispose` now runs through the same internal mutation guard as initialization and hydration hooks.
7. **Typed UI contract tests updated**
- Tests inspect parsed typed props and `outputs {}` declarations instead of matching legacy untyped declarations and `@event` lines.
- Dropdown danger styling is asserted through its canonical `data-danger="true"` state.
8. **Component reference generation corrected**
- Output payload parsing now supports nested parentheses and object/union types.
- RangeSlider now documents `input`, `change`, `focus`, and `blur`.
- FeatureGrid now includes the universal `size` and `color` props.
## Focused validation performed
- 250 WRN UI/showcase files parsed, generated, and browser-module syntax checked.
- 108 UI browser modules generated without syntax errors.
- Unknown lifecycle hook rejection passed.
- Reserved output/prop browser codegen probe passed.
- Synchronous non-store page generation probe passed.
- Store clientInit/dispose mutation probe passed.
- UI component reference exactly matches parsed public outputs.
- Component reference regeneration is idempotent.
- VS Code extension: 28 tests passed and all validation checks passed.
- `node scripts/validate-0.6.mjs`: 15 passed, 0 failed; Bun warning only.
The complete Bun workspace test suite must still be run on a machine with installed dependencies.
-28
View File
@@ -1,28 +0,0 @@
# WRNexusJS v0.6.0 R8 UI compile fixes
R8 fixes the remaining UI test cascade reported after R7.
## Root causes
1. Readonly-prop diagnostics treated comparison operators such as `===` as assignments.
2. Text inside JavaScript string literals, including selectors such as `input[name='...']`, was scanned as executable assignment syntax.
3. Function parameters that intentionally shadowed prop names were treated as mutations of those props.
4. `component-catalog.json` did not contain four bundled declarations even though the generated component reference did.
## Changes
- Added JavaScript trivia masking before readonly-prop mutation analysis.
- Assignment recognition now distinguishes `=`, compound assignments, `++`, and `--` from `==`, `===`, `=>`, and comparisons.
- `props.name = ...` is still rejected.
- Bare prop assignment is ignored when the name is a function parameter or local variable.
- Syntax and typecheck packages now share the same readonly-prop mutation detector.
- Component catalog generation now includes every bundled component while preserving existing metadata.
- Added R8 validation gates that compile all 108 UI components, verify native attribute forwarding, and compare catalog/reference declarations.
## Focused validation
- 108/108 UI components compile.
- 108/108 UI components include native attribute forwarding.
- Component catalog: 108 declarations.
- Component reference: 108 declarations.
- Generated showcase: 108 detail pages, 417 demos, 11 categories.
-25
View File
@@ -1,25 +0,0 @@
# WRNexusJS v0.6.0 R9 — ToggleCount animation stability
R9 fixes the final full-suite failure in `ToggleCount`.
## Root cause
The previous animation scheduled the next frame only after the preceding timeout fired. Under a heavily loaded full test suite, timer drift accumulated across every frame and the animation could finish later than `animationDuration`.
## Fix
- Every animation frame is scheduled immediately against its absolute offset within `animationDuration`.
- The displayed value remains at the previous value synchronously after a toggle.
- Every intermediate value is rounded to an integer.
- The final value is independently scheduled at exactly `animationDuration`.
- Animation tokens prevent stale timers from an earlier toggle overwriting a newer selection.
- Browser modules now bind peer client/shared functions through `context.functions`, so generated functions can safely call one another.
- `animateValueAt` is classified as a client function because it touches DOM nodes and schedules browser timers.
## Local validation
```powershell
bun install
bun run validate:0.6
bun run check
```
-18
View File
@@ -1,18 +0,0 @@
# WRNexusJS 0.6.0 syntax package root export fix
The v0.6 parser helpers are implementation details of `@wrnexus/syntax`, not a separate public versioned package surface.
## Canonical usage
```ts
import {
parseRuntimeFunctions,
parseStateDeclarations,
parseOutputs,
stripRuntimeFunctionModifiers,
} from "@wrnexus/syntax";
```
The compiler now imports `stripRuntimeFunctionModifiers` from `@wrnexus/syntax`. The `./v060` package export and the VS Code standalone-bundle alias were removed. `packages/syntax/src/v060.ts` remains an internal source module and its public APIs are re-exported by `packages/syntax/src/index.ts`.
This keeps application and framework imports stable across future releases and avoids exposing version numbers in package import paths.
-17
View File
@@ -1,17 +0,0 @@
# WRNexusJS v0.6.0 typecheck correction R2
This correction addresses the first local Bun validation failures reported after the focused issues 1-7 build.
## Corrected
- Added a valid root `.vscode/settings.json` so the v0.6 JSON validator succeeds.
- Store definitions now model shared, client-only, and server-only state as separate inferred object types.
- Store actions no longer infer as `never`; action methods remain callable from returned store instances.
- Renamed the internal initialization promise from `ready` to `whenReady` so application state may safely declare a field named `ready`.
- Updated generated browser-store modules to use the same collision-safe `whenReady` field.
## Focused validation
- `packages/store/test/store.test.ts` passes strict TypeScript checking with a local Bun test declaration shim.
- A live Node type-stripping probe passed server/client action dispatch, computed state, server-state exclusion, and the `ready` state collision case.
- `node scripts/validate-0.6.mjs` reports 12 passed, 0 failed in the packaging environment; only the expected warning remains because Bun is unavailable there.
-11
View File
@@ -1,11 +0,0 @@
# WRNexusJS 0.6.0 VS Code compiler bundle fix
The standalone VS Code compiler bundle must resolve the public syntax package through:
```text
@wrnexus/syntax -> packages/syntax/src/index.ts
```
The v0.6 syntax helpers are re-exported by `packages/syntax/src/index.ts`. No public or bundled import uses a version-specific `@wrnexus/syntax/v060` path.
This makes the editor compiler self-contained while keeping the package API stable.
-30
View File
@@ -1,30 +0,0 @@
# WRNexusJS 0.7.0 R1 — Local environment audit boundary
## Problem
The security audit recursively scanned every file in the working directory. Local, ignored environment files such as `examples/basic-app/.env` and `.env.uat` therefore failed the release audit even though they were not tracked by Git and would not be published.
## Fix
- Release security checks now scan `git ls-files -z` when Git metadata is available.
- Secret-like files tracked by Git still fail with `SEC-NO-TRACKED-SECRET-FILES`.
- Ignored or untracked local secret files produce `SEC-LOCAL-SECRET-FILES` warnings only.
- Source archives without `.git` metadata still scan every included file.
- The JSON report schema is updated to version 2 and includes warnings separately from errors.
## Existing repositories
If old `.env` files are still tracked, remove them from the Git index while retaining local copies:
```powershell
git rm --cached examples/basic-app/.env examples/basic-app/.env.uat
git add examples/basic-app/.env.example examples/basic-app/.env.uat.example .gitignore
git commit -m "security(examples): stop tracking local environment files"
```
The root `.gitignore` now also explicitly allows environment templates for named environments:
```gitignore
!.env.example
!.env.*.example
```
-19
View File
@@ -1,19 +0,0 @@
# WRNexusJS 0.7.0 R2 — Root Typecheck Fix
## Cause
The R1 source archive accidentally contained two temporary files used only for a dependency-free focused compile:
- `focus-shims.d.ts`
- `tsconfig.focus.json`
The root `tsc --noEmit` command automatically included `focus-shims.d.ts`. Its intentionally broad declarations replaced or merged with the installed Bun, Node.js, TypeScript, Happy DOM, filesystem, child-process, and build-tool declarations. That caused callback parameters to become implicit `any`, removed real Bun APIs and generics, and produced the reported 78 errors across otherwise valid source files.
## Fix
- Removed `focus-shims.d.ts` from the source tree.
- Removed `tsconfig.focus.json` from the source tree.
- Added a `validate:0.7` release guard that fails if either temporary file is present.
- Re-ran the root TypeScript program with typed Bun/Node/TypeScript module declarations.
No public framework API or runtime behavior changed in this correction.
-43
View File
@@ -1,43 +0,0 @@
# WRNexusJS 0.7.0 R3 — Workspace Repair and Typecheck Isolation
## Problem
Applying a source ZIP over an existing Git checkout cannot delete files or change the Git index. Two local focused-typecheck files and two previously tracked environment files therefore survived earlier archive updates:
- `focus-shims.d.ts`
- `tsconfig.focus.json`
- `examples/basic-app/.env`
- `examples/basic-app/.env.uat`
The ambient declarations in `focus-shims.d.ts` replaced or weakened the installed Bun, Node.js, TypeScript, filesystem, child-process, Happy DOM, and tsup typings. That produced dozens of false `implicit any`, missing namespace member, and untyped `Bun.serve` errors.
## Corrections
1. Root `tsconfig.json` now has explicit include/exclude rules and always excludes focused-typecheck helper files.
2. Added `bun run repair:workspace` to:
- remove the two temporary typecheck files;
- remove unsafe secret-like files and temporary shims from Git tracking;
- preserve local `.env` file contents;
- ensure the required `.gitignore` rules exist.
3. Added `bun run check:workspace` for non-mutating CI/release verification.
4. `validate:0.7` warns about local leftovers instead of allowing a TypeScript cascade, and verifies that the root typecheck excludes them.
5. The security audit blocks tracked typecheck shims and tracked environment files with the repair command in its error message.
6. Release prepare/publish now runs `check:workspace` before validation.
7. Package staging rejects `focus-shims.d.ts` and `tsconfig.focus.json`.
8. Added an executable Git regression test proving local environment values are retained while unsafe files are removed from tracking.
## Required one-time command for an existing checkout
```powershell
bun run repair:workspace
```
Then review and commit the index changes:
```powershell
git status
git add .gitignore package.json tsconfig.json scripts
git commit -m "fix(tooling): isolate temporary typecheck files and repair workspace state"
git push origin main
```
-29
View File
@@ -1,29 +0,0 @@
# WRNexusJS 0.7.0 R4 — Happy DOM Event Type Correction
## Problem
`packages/validation/test/validation.test.ts` returned the browser DOM `Event` type from its synthetic event helper. Happy DOM elements require Happy DOM's own event class, so strict TypeScript reported two incompatible `dispatchEvent()` calls.
## Fix
The helper now derives its event type directly from Happy DOM's own `document.dispatchEvent` parameter:
```ts
type HappyDOMEvent = Parameters<Window["document"]["dispatchEvent"]>[0];
type HappyDOMEventConstructor = new (type: string, init?: EventInit) => HappyDOMEvent;
function windowEvent(win: Window, type: string, init?: EventInit): HappyDOMEvent {
const EventConstructor = (win as unknown as { Event: HappyDOMEventConstructor }).Event;
return new EventConstructor(type, init);
}
```
This keeps browser DOM tests and Happy DOM tests type-safe without using an unsafe browser `Event` return type.
## Workspace repair
The two `.env` files and temporary focused-typecheck files shown by `check:workspace` are local leftovers. Run `bun run repair:workspace` once before the security audit. The command preserves local `.env` contents while removing unsafe files from Git tracking.
## Additional improvement
`check:workspace` now says `would remove` in check mode, instead of incorrectly implying files were already removed.
@@ -1,21 +0,0 @@
# WRNexusJS 0.7.0 R5 — Happy DOM element/event type alignment
## Problem
R4 returned Happy DOM `Event` objects from the test helper, but two inputs were still cast to the browser DOM `HTMLInputElement` type. TypeScript therefore rejected the Happy DOM event at `dispatchEvent()`.
## Correction
`packages/validation/test/validation.test.ts` now imports and consistently uses Happy DOM types for:
- `Event`
- `IEventInit`
- `HTMLElement`
- `HTMLInputElement`
- `HTMLFormElement`
The input, field, error and form nodes no longer use browser DOM global casts. Submit events also no longer cast back to the browser DOM `Event` type.
## Regression gate
`validate:0.7` now verifies that the validation tests use one Happy DOM type system and rejects the old `as unknown as HTMLInputElement`, `HTMLFormElement`, or `HTMLElement` casts.
-26
View File
@@ -1,26 +0,0 @@
# WRNexusJS 0.7.0 R6 - Lint cleanup
R6 fixes the remaining root ESLint findings reported after R5.
## Corrections
- Replaced control-character regular expressions in compiler, security URL validation, and syntax diagnostics with explicit ASCII character-code checks.
- `renderStoreHydration()` now actually uses `serializeForHtml()` for bounded, prototype-safe, HTML-safe hydration JSON.
- SBOM parse errors preserve the original caught error through `ErrorOptions.cause`.
- The workspace repair regression imports `node:process` explicitly.
- ESLint and Prettier ignore local `focus-shims.d.ts` and `tsconfig.focus.json` leftovers until `bun run repair:workspace` removes them.
- Added `validate:0.7` guards for every R6 regression.
## Security behavior retained
URLs containing ASCII whitespace, C0 control characters, or DEL are rejected by `@wrnexus/security`. Static compiler and syntax checks continue to normalize embedded C0 characters before detecting dangerous protocols such as `javascript:`.
## Local validation
Run on the target Bun workspace:
```powershell
bun run repair:workspace
bun install
bun run check
```
-40
View File
@@ -1,40 +0,0 @@
{
"release": "0.6.0",
"scope": "Specification issues 1 through 7",
"status": "passed-focused-validation",
"fullBunSuiteExecuted": false,
"results": {
"frameworkPackages": 33,
"uiComponents": 108,
"generatedUiBrowserModules": 108,
"browserModuleSyntaxFailures": 0,
"wrnFilesParsed": 250,
"wrnParseFailures": 0,
"unknownTypesInsideUiOutputContracts": 0,
"showcaseComponentPages": 108,
"showcaseLiveDemos": 417,
"showcaseCategories": 11,
"vscodeNodeTestsPassed": 28,
"repositoryValidatorPassed": 12,
"repositoryValidatorWarnings": 1,
"repositoryValidatorFailed": 0
},
"focusedProbes": {
"browserStoreRpc": "passed",
"persistenceMigration": "passed",
"persistenceValidation": "passed",
"storeHmr": "passed",
"legacyImportMode": "passed",
"compatibleImportMode": "passed",
"explicitImportMode": "passed",
"restrictedComponentRpcManifest": "passed",
"restrictedStoreRpcManifest": "passed",
"generatedServerStoreTypeScript": "passed"
},
"remainingReleaseGate": [
"bun install",
"bun run validate:0.6",
"bun run check",
"bun run --cwd examples/component-showcase check"
]
}
-20
View File
@@ -1,20 +0,0 @@
{
"release": "0.7.0-r1",
"fix": "Git-tracked secret-file audit boundary",
"results": {
"validate_0_7": {
"passed": 35,
"failed": 0
},
"clean_archive_security_audit": "passed",
"ignored_local_env_files": {
"result": "warning-only",
"exitCode": 0
},
"git_tracked_env_files": {
"result": "blocked",
"exitCode": 1
},
"javascript_syntax": "passed"
}
}
-30
View File
@@ -1,30 +0,0 @@
{
"release": "0.7.0-r2",
"reportedTypeScriptErrors": 78,
"reportedFiles": 28,
"rootCause": [
"focus-shims.d.ts was accidentally included in the release source and automatically loaded by the root TypeScript program",
"tsconfig.focus.json was a temporary packaging-only configuration and should not have been distributed"
],
"removedFiles": ["focus-shims.d.ts", "tsconfig.focus.json"],
"hardening": [
"validate:0.7 now rejects temporary focused-typecheck files",
"compressed response bodies are copied into a concrete ArrayBuffer for TypeScript 5.9 DOM compatibility",
"Happy DOM event constructors are accessed through explicit typed window shapes",
"Redis tests use the typed Bun global directly",
"package publishing imports node:process explicitly"
],
"validation": {
"rootTypeScriptProgram": "passed",
"validate07": { "passed": 36, "failed": 0 },
"securityFramework": { "passed": 10, "failed": 0 },
"validate06Compatibility": { "passed": 20, "warnings": 1, "failed": 0 },
"vscodeTests": { "passed": 28, "failed": 0 },
"vscodeValidation": "passed",
"temporaryFileGuard": {
"temporaryFilePresentExitCode": 1,
"temporaryFileAbsentExitCode": 0
}
},
"localAuthoritativeCommand": "bun run check"
}
-28
View File
@@ -1,28 +0,0 @@
{
"release": "0.7.0-r3",
"focus": "workspace repair, tracked secret cleanup, and root typecheck isolation",
"results": {
"validate_0_7": {
"passed": 37,
"warnings": 0,
"failed": 0
},
"security_framework": "passed",
"validate_0_6_compatibility": {
"passed": 20,
"warnings": 1,
"failed": 0
},
"workspace_repair_git_regression": "passed",
"package_staging_integrity": "passed",
"temporary_shim_excluded_from_root_typescript_program": "passed",
"vscode_tests": {
"passed": 28,
"failed": 0
},
"vscode_extension_validation": "passed",
"sbom_components": 339
},
"local_full_bun_check_required": true,
"required_command": "bun run repair:workspace && bun run check"
}
-26
View File
@@ -1,26 +0,0 @@
{
"release": "0.7.0-r4",
"fix": "Happy DOM synthetic event type compatibility",
"results": {
"validate_0_7": {
"passed": 38,
"warnings": 0,
"failed": 0
},
"security_framework_audit": "passed",
"workspace_repair_regression": "passed",
"validate_0_6_compatibility": {
"passed": 20,
"warnings": 1,
"failed": 0
},
"vscode_tests": {
"passed": 28,
"failed": 0
},
"vscode_extension_validation": "passed",
"happy_dom_event_type_structural_compile": "passed",
"zip_integrity": "passed"
},
"environment_limit": "Bun and the repository dependency tree are unavailable in the packaging environment, so the complete bun run check remains required on the target Windows workspace."
}

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