feat: add helpers and improve workspace auth flows

This commit is contained in:
2026-07-13 13:53:36 +05:30
parent 88e907783a
commit b4e5fade19
74 changed files with 853 additions and 131 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.16", "version": "0.2.17",
"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",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+8
View File
@@ -34,6 +34,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. | | `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. |
| `wrnexus create <app-name>` | Scaffold a new single app from an inline template. | | `wrnexus create <app-name>` | Scaffold a new single app from an inline template. |
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | | `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
@@ -167,6 +168,13 @@ wrnexus workspace acme
wrnexus gateway --port=3000 wrnexus gateway --port=3000
``` ```
From a workspace root, add and register another app in one command:
```bash
wrnexus workspace add reports --domain=reports.localhost
bun install
```
Development gateways bind to `127.0.0.1` by default for reliable access on Windows, Development gateways bind to `127.0.0.1` by default for reliable access on Windows,
macOS, and Linux. Open the configured app domain on the gateway port (for example macOS, and Linux. Open the configured app domain on the gateway port (for example
`http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports `http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.", "description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -28,16 +28,16 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16", "@wrnexus/core": "^0.2.17",
"@wrnexus/router": "^0.2.16", "@wrnexus/router": "^0.2.17",
"@wrnexus/csr": "^0.2.16", "@wrnexus/csr": "^0.2.17",
"@wrnexus/compiler": "^0.2.16", "@wrnexus/compiler": "^0.2.17",
"@wrnexus/styles": "^0.2.16", "@wrnexus/styles": "^0.2.17",
"@wrnexus/dev-server": "^0.2.16", "@wrnexus/dev-server": "^0.2.17",
"@wrnexus/ui": "^0.2.16", "@wrnexus/ui": "^0.2.17",
"@wrnexus/validation": "^0.2.16", "@wrnexus/validation": "^0.2.17",
"@wrnexus/i18n": "^0.2.16", "@wrnexus/i18n": "^0.2.17",
"@wrnexus/db": "^0.2.16" "@wrnexus/db": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.", "description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16" "@wrnexus/core": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+13
View File
@@ -176,6 +176,19 @@ interface GatewaySecurity {
} }
``` ```
Forward auth is a verification hook, not a login page. Configure `forward.url` with a
dedicated endpoint such as `http://sso.localhost:3000/api/verify`. The gateway forwards
the request's `Cookie` and `Authorization` headers plus `X-Forwarded-Host`,
`X-Forwarded-Proto`, `X-Original-Method`, and `X-Original-Uri` (including its query
string). The verifier must return 2xx only for an authenticated session and 401/403
otherwise. Pointing forward auth at an SSO home page that always returns 200 allows
every request and does not implement SSO.
For browser SSO, the verifier may return a `302`/`303`/`307`/`308` with a `Location`
header pointing to its login page. The gateway passes that redirect to the browser. The
login flow should validate a signed `returnTo` value before redirecting back; API clients
should receive `401`/`403` instead of an HTML login redirect.
Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal
port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a
`RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need `RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need
+13 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.16", "version": "0.2.17",
"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",
@@ -25,18 +25,18 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16", "@wrnexus/core": "^0.2.17",
"@wrnexus/router": "^0.2.16", "@wrnexus/router": "^0.2.17",
"@wrnexus/ssr": "^0.2.16", "@wrnexus/ssr": "^0.2.17",
"@wrnexus/csr": "^0.2.16", "@wrnexus/csr": "^0.2.17",
"@wrnexus/compiler": "^0.2.16", "@wrnexus/compiler": "^0.2.17",
"@wrnexus/styles": "^0.2.16", "@wrnexus/styles": "^0.2.17",
"@wrnexus/ui": "^0.2.16", "@wrnexus/ui": "^0.2.17",
"@wrnexus/validation": "^0.2.16", "@wrnexus/validation": "^0.2.17",
"@wrnexus/i18n": "^0.2.16", "@wrnexus/i18n": "^0.2.17",
"@wrnexus/db": "^0.2.16", "@wrnexus/db": "^0.2.17",
"@wrnexus/pubsub": "^0.2.16", "@wrnexus/pubsub": "^0.2.17",
"@wrnexus/uploader": "^0.2.16" "@wrnexus/uploader": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+60
View File
@@ -0,0 +1,60 @@
# @wrnexus/helpers
Safe convenience helpers for common WrNexus application flows. The package uses
standard `Context`, `URL`, and `Response` values and has no runtime dependency beyond
`@wrnexus/core`.
## Installation
```bash
bun add @wrnexus/helpers
```
The package is private, so the machine must be authenticated to the `wrnexus` npm
organization.
## Forward-auth login redirects
The gateway calls an SSO verifier on a different URL from the original application.
These helpers reconstruct the original URL from the gateway headers and safely place it
in the login redirect:
```ts
import type { Context } from "@wrnexus/core";
import { redirectToLogin } from "@wrnexus/helpers";
export const GET = async (ctx: Context) => {
if (await hasValidSession(ctx)) {
return new Response(null, { status: 204 });
}
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
};
```
This creates a response such as:
```text
Location: http://sso.localhost:3000/login?returnTo=http%3A%2F%2Fadmin.localhost%3A3000%2F
```
Always list the application hosts that are valid redirect destinations. Forwarded host
headers are rejected when `allowedHosts` is absent or does not match, preventing an open
redirect. A callback can support dynamic tenant domains:
```ts
allowedHosts: (host) => host.endsWith(".example.test");
```
## API
- `getOriginalRequestUrl(ctx, options): URL` — reconstruct the gateway URL.
- `getOriginalRequestOrigin(ctx, options): string` — return only its origin.
- `getOriginalRequestPath(ctx): string` — return its path and query string.
- `getOriginalRequestMethod(ctx): string` — return its HTTP method.
- `redirectToLogin(ctx, loginUrl, options): Response` — create a login redirect with an
encoded `returnTo` parameter.
For direct requests without gateway headers, URL helpers use `ctx.url`.
+29
View File
@@ -0,0 +1,29 @@
{
"name": "@wrnexus/helpers",
"version": "0.2.17",
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"engines": {
"bun": ">=1.1.0"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "restricted"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"dependencies": {
"@wrnexus/core": "^0.2.17"
},
"files": [
"dist"
]
}
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.", "description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16" "@wrnexus/core": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.", "description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/native": "^0.2.16" "@wrnexus/native": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.", "description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.", "description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.", "description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.", "description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.", "description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,8 +21,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/compiler": "^0.2.16", "@wrnexus/compiler": "^0.2.17",
"@wrnexus/core": "^0.2.16" "@wrnexus/core": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.", "description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16" "@wrnexus/core": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.", "description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.2.16" "@wrnexus/uploader": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -22,7 +22,7 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16" "@wrnexus/core": "^0.2.17"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.", "description": "@wrnexus/uploader — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.16" "@wrnexus/core": "^0.2.17"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.", "description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -1
View File
@@ -1384,10 +1384,11 @@ self-skips without `WRNEXUS_PG_URL` / `WRNEXUS_MYSQL_URL`.
## Optional packages ## Optional packages
Opt-in, dependency-free packages (Bun/WebCrypto natives) — import only what you need: Opt-in helpers and feature packages — import only what you need:
| Package | What it gives you | | Package | What it gives you |
| --------------------- | ------------------------------------------------------------------------------------- | | --------------------- | ------------------------------------------------------------------------------------- |
| `@wrnexus/helpers` | Context URL helpers and safe forward-auth `redirectToLogin` responses |
| `@wrnexus/jwt` | HS256 `signJwt`/`verifyJwt` + `jwtAuth` bearer middleware (stateless auth) | | `@wrnexus/jwt` | HS256 `signJwt`/`verifyJwt` + `jwtAuth` bearer middleware (stateless auth) |
| `@wrnexus/oauth` | OAuth 2.0 sign-in with PKCE — Google/GitHub/Discord presets + `defineProvider` | | `@wrnexus/oauth` | OAuth 2.0 sign-in with PKCE — Google/GitHub/Discord presets + `defineProvider` |
| `@wrnexus/authz` | Authorization: RBAC (`defineRbac`), PBAC/ABAC policies (`any`/`all`/`attr`), guards | | `@wrnexus/authz` | Authorization: RBAC (`defineRbac`), PBAC/ABAC policies (`any`/`all`/`attr`), guards |
@@ -1399,6 +1400,7 @@ Opt-in, dependency-free packages (Bun/WebCrypto natives) — import only what yo
```ts ```ts
import { signJwt, jwtAuth } from "@wrnexus/jwt"; import { signJwt, jwtAuth } from "@wrnexus/jwt";
import { getOriginalRequestUrl, redirectToLogin } from "@wrnexus/helpers";
import { google, startAuth, completeAuth } from "@wrnexus/oauth"; import { google, startAuth, completeAuth } from "@wrnexus/oauth";
import { defineRbac, requirePermission, any, attr } from "@wrnexus/authz"; import { defineRbac, requirePermission, any, attr } from "@wrnexus/authz";
import { encrypt, decrypt, generateKey, sha256, hmacSign } from "@wrnexus/encryption"; import { encrypt, decrypt, generateKey, sha256, hmacSign } from "@wrnexus/encryption";
+34 -25
View File
@@ -35,15 +35,15 @@
}, },
"packages/ai": { "packages/ai": {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/authz": { "packages/authz": {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/cli": { "packages/cli": {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.11", "version": "0.2.17",
"bin": { "bin": {
"wrnexus": "src/index.ts", "wrnexus": "src/index.ts",
}, },
@@ -62,26 +62,26 @@
}, },
"packages/compiler": { "packages/compiler": {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/core": { "packages/core": {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/csr": { "packages/csr": {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/db": { "packages/db": {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/dev-server": { "packages/dev-server": {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/compiler": "workspace:*", "@wrnexus/compiler": "workspace:*",
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
@@ -99,49 +99,56 @@
}, },
"packages/encryption": { "packages/encryption": {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.11", "version": "0.2.17",
},
"packages/helpers": {
"name": "@wrnexus/helpers",
"version": "0.2.17",
"dependencies": {
"@wrnexus/core": "workspace:*",
},
}, },
"packages/i18n": { "packages/i18n": {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/jwt": { "packages/jwt": {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/mobile": { "packages/mobile": {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/native": "workspace:*", "@wrnexus/native": "workspace:*",
}, },
}, },
"packages/native": { "packages/native": {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/oauth": { "packages/oauth": {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/pubsub": { "packages/pubsub": {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/queue": { "packages/queue": {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/reactive": { "packages/reactive": {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/router": { "packages/router": {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/compiler": "workspace:*", "@wrnexus/compiler": "workspace:*",
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
@@ -149,43 +156,43 @@
}, },
"packages/ssr": { "packages/ssr": {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/styles": { "packages/styles": {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/uploader": "workspace:*", "@wrnexus/uploader": "workspace:*",
}, },
}, },
"packages/test": { "packages/test": {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/tracking": { "packages/tracking": {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.11", "version": "0.2.17",
}, },
"packages/ui": { "packages/ui": {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/uploader": { "packages/uploader": {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.11", "version": "0.2.17",
"dependencies": { "dependencies": {
"@wrnexus/core": "workspace:*", "@wrnexus/core": "workspace:*",
}, },
}, },
"packages/validation": { "packages/validation": {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.11", "version": "0.2.17",
}, },
}, },
"overrides": { "overrides": {
@@ -440,6 +447,8 @@
"@wrnexus/encryption": ["@wrnexus/encryption@workspace:packages/encryption"], "@wrnexus/encryption": ["@wrnexus/encryption@workspace:packages/encryption"],
"@wrnexus/helpers": ["@wrnexus/helpers@workspace:packages/helpers"],
"@wrnexus/i18n": ["@wrnexus/i18n@workspace:packages/i18n"], "@wrnexus/i18n": ["@wrnexus/i18n@workspace:packages/i18n"],
"@wrnexus/jwt": ["@wrnexus/jwt@workspace:packages/jwt"], "@wrnexus/jwt": ["@wrnexus/jwt@workspace:packages/jwt"],
+6 -1
View File
@@ -1020,8 +1020,11 @@ page Users {
## 27. Optional packages ## 27. Optional packages
Dependencyfree (Bun/WebCrypto). Import only what you need. Convenience helpers and dependency-free feature packages. Import only what you need.
- **`@wrnexus/helpers`** — context helpers including `getOriginalRequestUrl`,
`getOriginalRequestOrigin`, `getOriginalRequestPath`, `getOriginalRequestMethod`, and the safe
forward-auth `redirectToLogin(ctx, "/login", { allowedHosts })` response helper.
- **`@wrnexus/jwt`** — `signJwt(payload, secret, { expiresIn?, now? })`, `verifyJwt(token, secret)` - **`@wrnexus/jwt`** — `signJwt(payload, secret, { expiresIn?, now? })`, `verifyJwt(token, secret)`
(throws `JwtError`), `jwtAuth({ secret, getToken?, required? })` middleware. (throws `JwtError`), `jwtAuth({ secret, getToken?, required? })` middleware.
- **`@wrnexus/oauth`** — PKCE OAuth 2.0. Presets `google/github/discord(creds)` + `defineProvider`. - **`@wrnexus/oauth`** — PKCE OAuth 2.0. Presets `google/github/discord(creds)` + `defineProvider`.
@@ -1226,6 +1229,7 @@ wrnexus dev [app-dir] [--port=3000] [--profile=<name>] Dev server (live reload
wrnexus build [app-dir] [--profile=<name>] Production bundle → dist/ (production profile by default) wrnexus build [app-dir] [--profile=<name>] Production bundle → dist/ (production profile by default)
wrnexus create <app-name> Scaffold a new app wrnexus create <app-name> Scaffold a new app
wrnexus workspace <name> Scaffold a monorepo (apps/* + shared packages/*) wrnexus workspace <name> Scaffold a monorepo (apps/* + shared packages/*)
wrnexus workspace add <name> [--domain=name.localhost] Add an app to the current workspace
wrnexus gateway [--port=3000] [--prod] Serve every workspace app behind one port, routed by domain wrnexus gateway [--port=3000] [--prod] Serve every workspace app behind one port, routed by domain
wrnexus generate <type> <name> (alias: g) Scaffold: page | component | api | schema wrnexus generate <type> <name> (alias: g) Scaffold: page | component | api | schema
wrnexus generate routes Regenerate app/routes.gen.ts wrnexus generate routes Regenerate app/routes.gen.ts
@@ -1353,6 +1357,7 @@ directive to `false`/`null` to remove it.
- `@wrnexus/dev-server``startServer`, `startGateway` (multi-app host router), prod handler. - `@wrnexus/dev-server``startServer`, `startGateway` (multi-app host router), prod handler.
- `@wrnexus/cli` — the `wrnexus` binary; subpath `/workspace` (`WorkspaceConfig`, `startGateway` wiring). - `@wrnexus/cli` — the `wrnexus` binary; subpath `/workspace` (`WorkspaceConfig`, `startGateway` wiring).
- `@wrnexus/validation``v`, `parseBody`, `parseEnv`, `invalid`, `renderSchemasScript`. - `@wrnexus/validation``v`, `parseBody`, `parseEnv`, `invalid`, `renderSchemasScript`.
- `@wrnexus/helpers` — original gateway URL/path/method helpers and `redirectToLogin`.
- `@wrnexus/ui``uiComponentsDir`, `uiCssPath`, `uiCss`, `uiComponentNames`. - `@wrnexus/ui``uiComponentsDir`, `uiCssPath`, `uiCss`, `uiComponentNames`.
- `@wrnexus/i18n``loadLocales`, `resolveI18n`, `makeT`, `resolveLang`, `translateHtml`, - `@wrnexus/i18n``loadLocales`, `resolveI18n`, `makeT`, `resolveLang`, `translateHtml`,
`formatNumber`, `formatCurrency`, `formatDate`, `formatRelativeTime`, `plural`. `formatNumber`, `formatCurrency`, `formatDate`, `formatRelativeTime`, `plural`.
+1 -1
View File
@@ -1,6 +1,6 @@
# Publishing WrNexus to npm # Publishing WrNexus to npm
All 25 `@wrnexus/*` packages are built and published to the **public npm registry** All 26 `@wrnexus/*` packages are built and published to the **public npm registry**
(`registry.npmjs.org`), then installed on any machine with **Bun**. (`registry.npmjs.org`), then installed on any machine with **Bun**.
## Prerequisites ## Prerequisites
+17
View File
@@ -160,6 +160,21 @@ export const POST = async (ctx) => {
`req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`), `req: Request`, `url: URL`, `params: Record<string,string>` (dynamic route params, e.g. `/users/[id]` → `ctx.params.id`),
`lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`. `lang: string`, `t(key, params?)` (i18n), `cookies` (get/set), `session` (get/set). Auth: `getUser(ctx)` after `sessionAuth`/`logIn`.
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
`@wrnexus/helpers` instead of constructing it from untrusted headers:
```ts
import { redirectToLogin } from "@wrnexus/helpers";
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
```
The package also exports `getOriginalRequestUrl`, `getOriginalRequestOrigin`,
`getOriginalRequestPath`, and `getOriginalRequestMethod`. Always pass `allowedHosts` when
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
## Middleware & realtime ## Middleware & realtime
```ts ```ts
@@ -243,7 +258,9 @@ export const POST = async (ctx) => {
``` ```
wrnexus dev . # dev server + HMR wrnexus dev . # dev server + HMR
wrnexus build . # production build → dist/server.js wrnexus build . # production build → dist/server.js
bun dist/server.js # run the production server (or npm start)
wrnexus create <name> # scaffold a new app wrnexus create <name> # scaffold a new app
wrnexus update --latest # deps + syntax/config migrations + verification
wrnexus generate page <Name> # scaffold a page (aliases: g p) wrnexus generate page <Name> # scaffold a page (aliases: g p)
wrnexus generate component <name> | api <path> | schema <name> wrnexus generate component <name> | api <path> | schema <name>
wrnexus db migrate | rollback | status | new [--from-models] | generate | seed wrnexus db migrate | rollback | status | new [--from-models] | generate | seed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+8
View File
@@ -34,6 +34,7 @@ Every command accepts an optional `[app-dir]` (defaults to `.`). Commands that r
| `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. | | `wrnexus build [app-dir]` | Build a self-contained production server bundle + assets into `dist/`. |
| `wrnexus create <app-name>` | Scaffold a new single app from an inline template. | | `wrnexus create <app-name>` | Scaffold a new single app from an inline template. |
| `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). | | `wrnexus workspace <name>` | Scaffold a monorepo (`apps/*` + shared `packages/*`). |
| `wrnexus workspace add <name>` | Add and register an app in the current workspace. |
| `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. | | `wrnexus gateway [--port=3000]` | Serve every workspace app behind one port, routed by domain. |
| `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. | | `wrnexus generate <type> <name>` | Scaffold a `page` \| `component` \| `api` \| `schema`. |
| `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). | | `wrnexus generate routes` | Regenerate the typed routes file (`app/routes.gen.ts`). |
@@ -167,6 +168,13 @@ wrnexus workspace acme
wrnexus gateway --port=3000 wrnexus gateway --port=3000
``` ```
From a workspace root, add and register another app in one command:
```bash
wrnexus workspace add reports --domain=reports.localhost
bun install
```
Development gateways bind to `127.0.0.1` by default for reliable access on Windows, Development gateways bind to `127.0.0.1` by default for reliable access on Windows,
macOS, and Linux. Open the configured app domain on the gateway port (for example macOS, and Linux. Open the configured app domain on the gateway port (for example
`http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports `http://localhost:3000` or `http://admin.localhost:3000`), not the internal child ports
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+15
View File
@@ -167,6 +167,21 @@ export const POST = async (ctx) => {
\`req: Request\`, \`url: URL\`, \`params: Record<string,string>\` (dynamic route params, e.g. \`/users/[id]\`\`ctx.params.id\`), \`req: Request\`, \`url: URL\`, \`params: Record<string,string>\` (dynamic route params, e.g. \`/users/[id]\`\`ctx.params.id\`),
\`lang: string\`, \`t(key, params?)\` (i18n), \`cookies\` (get/set), \`session\` (get/set). Auth: \`getUser(ctx)\` after \`sessionAuth\`/\`logIn\`. \`lang: string\`, \`t(key, params?)\` (i18n), \`cookies\` (get/set), \`session\` (get/set). Auth: \`getUser(ctx)\` after \`sessionAuth\`/\`logIn\`.
When an SSO forward-auth verifier needs the URL that originally reached the gateway, use
\`@wrnexus/helpers\` instead of constructing it from untrusted headers:
\`\`\`ts
import { redirectToLogin } from "@wrnexus/helpers";
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
\`\`\`
The package also exports \`getOriginalRequestUrl\`, \`getOriginalRequestOrigin\`,
\`getOriginalRequestPath\`, and \`getOriginalRequestMethod\`. Always pass \`allowedHosts\` when
using forwarded gateway URLs; the helper rejects untrusted redirect destinations.
## Middleware & realtime ## Middleware & realtime
\`\`\`ts \`\`\`ts
+2
View File
@@ -84,6 +84,7 @@ Thumbs.db
"dependencies": { "dependencies": {
"@wrnexus/ai": "${frameworkVersion}", "@wrnexus/ai": "${frameworkVersion}",
"@wrnexus/core": "${frameworkVersion}", "@wrnexus/core": "${frameworkVersion}",
"@wrnexus/helpers": "${frameworkVersion}",
"@wrnexus/styles": "${frameworkVersion}", "@wrnexus/styles": "${frameworkVersion}",
"@wrnexus/validation": "${frameworkVersion}", "@wrnexus/validation": "${frameworkVersion}",
"@wrnexus/db": "${frameworkVersion}" "@wrnexus/db": "${frameworkVersion}"
@@ -175,6 +176,7 @@ dist/
.wrnexus/ .wrnexus/
**/.wrnexus/ **/.wrnexus/
*.log *.log
CLAUDE.md
`, `,
".editorconfig": `root = true ".editorconfig": `root = true
+8 -2
View File
@@ -43,6 +43,8 @@ Usage:
wrnexus build [app-dir] Build a production server bundle + assets wrnexus build [app-dir] Build a production server bundle + assets
wrnexus create <app-name> Scaffold a new app wrnexus create <app-name> Scaffold a new app
wrnexus workspace <name> Scaffold a monorepo (apps/* + shared packages/*) wrnexus workspace <name> Scaffold a monorepo (apps/* + shared packages/*)
wrnexus workspace add <name> [--domain=name.localhost]
Add an app to the current workspace
wrnexus gateway [--port=3000] Serve every workspace app behind one port, routed by domain wrnexus gateway [--port=3000] Serve every workspace app behind one port, routed by domain
wrnexus generate <type> <name> Scaffold a page | component | api | schema wrnexus generate <type> <name> Scaffold a page | component | api | schema
wrnexus generate routes | docker | mobile wrnexus generate routes | docker | mobile
@@ -92,8 +94,12 @@ async function main(): Promise<void> {
createApp(rest[0] ?? ""); createApp(rest[0] ?? "");
break; break;
case "workspace": { case "workspace": {
const { createWorkspace } = await import("./workspace.ts"); const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts");
createWorkspace(rest.find((a) => !a.startsWith("--")) ?? ""); if (rest[0] === "add") {
await addWorkspaceApp(".", rest[1] ?? "", rest.slice(2));
} else {
createWorkspace(rest.find((a) => !a.startsWith("--")) ?? "");
}
break; break;
} }
case "gateway": { case "gateway": {
+137 -10
View File
@@ -1,6 +1,7 @@
/** /**
* Monorepo support: * Monorepo support:
* - `wrnexus workspace <name>` scaffolds a multi-app workspace (apps/* + shared packages/*) * - `wrnexus workspace <name>` scaffolds a multi-app workspace (apps/* + shared packages/*)
* - `wrnexus workspace add <name> --domain=<host>` adds an app to the current workspace
* - `wrnexus gateway [--port]` serves every app behind one port, routed by domain * - `wrnexus gateway [--port]` serves every app behind one port, routed by domain
* *
* A workspace holds several WrNexus apps under `apps/*` and shared libraries under * A workspace holds several WrNexus apps under `apps/*` and shared libraries under
@@ -9,7 +10,7 @@
* cross-process). `wrnexus.workspace.ts` maps each app to the domains it serves. * cross-process). `wrnexus.workspace.ts` maps each app to the domains it serves.
*/ */
import { existsSync, mkdirSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path"; import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { scaffoldApp } from "./create.ts"; import { scaffoldApp } from "./create.ts";
@@ -151,12 +152,139 @@ apps are internal gateway targets, not public workspace URLs.
## Add another app ## Add another app
\`\`\`bash \`\`\`bash
wrnexus create apps/reports wrnexus workspace add reports --domain=reports.localhost
# then add it to wrnexus.workspace.ts with its domains
\`\`\` \`\`\`
`, `,
}); });
const workspaceConfigNames = [
"wrnexus.workspace.ts",
"wrnexus.workspace.js",
"wrnexus.workspace.mjs",
] as const;
function workspaceConfigPath(root: string): string | null {
for (const file of workspaceConfigNames) {
const path = join(root, file);
if (existsSync(path)) return path;
}
return null;
}
/** Insert an app entry into the top-level `apps: [...]` array. */
export function insertWorkspaceApp(source: string, app: WorkspaceApp): string {
const declaration = /\bapps\s*:\s*\[/.exec(source);
if (!declaration) throw new Error("Workspace config has no `apps: [...]` array.");
const start = source.indexOf("[", declaration.index);
let depth = 0;
let quote = "";
let escaped = false;
let lineComment = false;
let blockComment = false;
let end = -1;
for (let i = start; i < source.length; i++) {
const char = source[i]!;
const next = source[i + 1] ?? "";
if (lineComment) {
if (char === "\n") lineComment = false;
continue;
}
if (blockComment) {
if (char === "*" && next === "/") {
blockComment = false;
i++;
}
continue;
}
if (quote) {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === quote) quote = "";
continue;
}
if (char === "/" && next === "/") {
lineComment = true;
i++;
continue;
}
if (char === "/" && next === "*") {
blockComment = true;
i++;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char === "[") depth++;
if (char === "]" && --depth === 0) {
end = i;
break;
}
}
if (end < 0) throw new Error("Workspace `apps` array is not closed.");
const serialized = `{ name: ${JSON.stringify(app.name)}, dir: ${JSON.stringify(app.dir)}, domains: ${JSON.stringify(app.domains)}${app.port ? `, port: ${app.port}` : ""} }`;
if (!source.slice(start, end).includes("\n")) {
const contents = source.slice(start + 1, end).trimEnd();
const separator = contents && !contents.endsWith(",") ? "," : "";
return source.slice(0, end) + `${separator} ${serialized}` + source.slice(end);
}
const lineStart = source.lastIndexOf("\n", end - 1) + 1;
const closeIndent = /^\s*/.exec(source.slice(lineStart, end))?.[0] ?? "";
const entry = `${closeIndent} ${serialized},\n`;
return source.slice(0, lineStart) + entry + source.slice(lineStart);
}
/** Add a scaffolded app and register its domain in the current workspace. */
export async function addWorkspaceApp(root: string, name: string, args: string[]): Promise<void> {
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
throw new Error(
"App name must start with a letter and contain only lowercase letters, digits, and hyphens.",
);
}
const resolvedRoot = resolve(root);
const configPath = workspaceConfigPath(resolvedRoot);
if (!configPath) throw new Error("No wrnexus.workspace.ts found in the current directory.");
const domain =
args.find((arg) => arg.startsWith("--domain="))?.split("=")[1] || `${name}.localhost`;
if (!/^[a-z0-9.-]+$/.test(domain)) throw new Error(`Invalid workspace domain: ${domain}`);
const portValue = args.find((arg) => arg.startsWith("--port="))?.split("=")[1];
const port = portValue ? Number(portValue) : undefined;
if (portValue && (!Number.isInteger(port) || port! < 1 || port! > 65535)) {
throw new Error(`Invalid app port: ${portValue}`);
}
const config = await loadWorkspaceConfig(resolvedRoot);
const dir = `apps/${name}`;
if (config.apps.some((app) => app.name === name || app.dir.replace(/\\/g, "/") === dir)) {
throw new Error(`Workspace app '${name}' already exists.`);
}
if (config.apps.some((app) => app.domains.includes(domain))) {
throw new Error(`Workspace domain '${domain}' is already assigned.`);
}
const appRoot = join(resolvedRoot, "apps", name);
if (existsSync(appRoot)) throw new Error(`Refusing to overwrite existing directory: ${appRoot}`);
const original = readFileSync(configPath, "utf8");
const next = insertWorkspaceApp(original, { name, dir, domains: [domain], port });
scaffoldApp(appRoot, name);
try {
writeFileSync(configPath, next, "utf8");
} catch (error) {
rmSync(appRoot, { recursive: true, force: true });
throw error;
}
console.log(`✓ Added app ${name}`);
console.log(` directory: ${dir}`);
console.log(` domain: http://${domain}:3000`);
console.log("\nRun `bun install`, then `bun run dev`.");
}
/** Scaffold a monorepo workspace with two starter apps + a shared package. */ /** Scaffold a monorepo workspace with two starter apps + a shared package. */
export function createWorkspace(name: string): void { export function createWorkspace(name: string): void {
if (!name) { if (!name) {
@@ -187,14 +315,13 @@ export function createWorkspace(name: string): void {
/** Load `wrnexus.workspace.ts` from a directory. */ /** Load `wrnexus.workspace.ts` from a directory. */
export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig> { export async function loadWorkspaceConfig(root: string): Promise<WorkspaceConfig> {
for (const file of ["wrnexus.workspace.ts", "wrnexus.workspace.js", "wrnexus.workspace.mjs"]) { const path = workspaceConfigPath(root);
const path = join(root, file); if (path) {
if (existsSync(path)) { const mod = (await import(pathToFileURL(path).href)) as { default?: WorkspaceConfig };
const mod = (await import(pathToFileURL(path).href)) as { default?: WorkspaceConfig }; if (!mod.default?.apps?.length) {
if (!mod.default?.apps?.length) throw new Error(`${path.split(/[\\/]/).at(-1)} must default-export { apps: [...] }`);
throw new Error(`${file} must default-export { apps: [...] }`);
return mod.default;
} }
return mod.default;
} }
throw new Error("No wrnexus.workspace.ts found. Run `wrnexus workspace <name>` to scaffold one."); throw new Error("No wrnexus.workspace.ts found. Run `wrnexus workspace <name>` to scaffold one.");
} }
+2
View File
@@ -61,12 +61,14 @@ test("scaffoldApp pins the current framework and exposes llms.txt publicly", ()
const version = currentCliVersion(); const version = currentCliVersion();
expect(pkg.wrnexus.version).toBe(version); expect(pkg.wrnexus.version).toBe(version);
expect(pkg.dependencies["@wrnexus/helpers"]).toBe(version);
for (const dependency of Object.values(pkg.dependencies) as string[]) { for (const dependency of Object.values(pkg.dependencies) as string[]) {
expect(dependency).toBe(version); expect(dependency).toBe(version);
} }
expect(pkg.devDependencies["@wrnexus/cli"]).toBe(version); expect(pkg.devDependencies["@wrnexus/cli"]).toBe(version);
expect(existsSync(join(root, "public", "llms.txt"))).toBe(true); expect(existsSync(join(root, "public", "llms.txt"))).toBe(true);
expect(existsSync(join(root, "llms.txt"))).toBe(false); expect(existsSync(join(root, "llms.txt"))).toBe(false);
expect(readFileSync(join(root, ".prettierignore"), "utf8")).toContain("CLAUDE.md");
expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toContain( expect(readFileSync(join(root, "app", "pages", "index.wrn"), "utf8")).toContain(
'href="/api/hello"', 'href="/api/hello"',
); );
+49 -1
View File
@@ -1,6 +1,9 @@
import { expect, test } from "bun:test"; import { expect, test } from "bun:test";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { currentCliVersion } from "../src/update-notifier.ts"; import { currentCliVersion } from "../src/update-notifier.ts";
import { workspaceFiles } from "../src/workspace.ts"; import { addWorkspaceApp, insertWorkspaceApp, workspaceFiles } from "../src/workspace.ts";
test("workspace templates pin the running framework release", () => { test("workspace templates pin the running framework release", () => {
const files = workspaceFiles("acme"); const files = workspaceFiles("acme");
@@ -13,3 +16,48 @@ test("workspace templates pin the running framework release", () => {
expect(files["README.md"]).toContain("http://127.0.0.1:3000"); expect(files["README.md"]).toContain("http://127.0.0.1:3000");
expect(files["README.md"]).toContain("internal gateway targets"); expect(files["README.md"]).toContain("internal gateway targets");
}); });
test("insertWorkspaceApp handles nested arrays and comments", () => {
const source = `const config = {
apps: [
{ name: "web", dir: "apps/web", domains: ["localhost"] },
{ name: "admin", dir: "apps/admin", domains: ["admin.localhost"], auth: { allowIps: ["::1"] } }, // ]
],
};
export default config;
`;
const result = insertWorkspaceApp(source, {
name: "reports",
dir: "apps/reports",
domains: ["reports.localhost"],
});
expect(result).toContain(
'{ name: "reports", dir: "apps/reports", domains: ["reports.localhost"] },',
);
expect(result.indexOf('name: "reports"')).toBeLessThan(result.indexOf("\n ],"));
});
test("workspace add scaffolds and registers an app", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-workspace-add-"));
writeFileSync(
join(root, "wrnexus.workspace.ts"),
'export default { apps: [{ name: "web", dir: "apps/web", domains: ["localhost"] }] };\n',
);
try {
await addWorkspaceApp(root, "reports", ["--domain=reports.localhost"]);
const manifest = JSON.parse(
readFileSync(join(root, "apps", "reports", "package.json"), "utf8"),
);
const config = readFileSync(join(root, "wrnexus.workspace.ts"), "utf8");
expect(manifest.name).toBe("reports");
expect(manifest.wrnexus.version).toBe(currentCliVersion());
expect(config).toContain('name: "reports"');
expect(config).toContain('domains: ["reports.localhost"]');
expect(existsSync(join(root, "apps", "reports", "public", "llms.txt"))).toBe(true);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+13
View File
@@ -176,6 +176,19 @@ interface GatewaySecurity {
} }
``` ```
Forward auth is a verification hook, not a login page. Configure `forward.url` with a
dedicated endpoint such as `http://sso.localhost:3000/api/verify`. The gateway forwards
the request's `Cookie` and `Authorization` headers plus `X-Forwarded-Host`,
`X-Forwarded-Proto`, `X-Original-Method`, and `X-Original-Uri` (including its query
string). The verifier must return 2xx only for an authenticated session and 401/403
otherwise. Pointing forward auth at an SSO home page that always returns 200 allows
every request and does not implement SSO.
For browser SSO, the verifier may return a `302`/`303`/`307`/`308` with a `Location`
header pointing to its login page. The gateway passes that redirect to the browser. The
login flow should validate a signed `returnTo` value before redirecting back; API clients
should receive `401`/`403` instead of an HTML login redirect.
Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal Open the gateway URL (normally `http://127.0.0.1:3000`), not an app's internal
port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a port. The gateway exposes `/__gateway/health` (JSON list of routed apps) and returns a
`RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need `RunningGateway` (`{ port, url, stop() }`). Use `--host=0.0.0.0` when other devices need
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+31 -8
View File
@@ -103,6 +103,35 @@ function timingSafeEqual(a: string, b: string): boolean {
return diff === 0; return diff === 0;
} }
/** Preserve an intentional verifier redirect while keeping other failures opaque. */
export function forwardAuthFailure(res: Response, verifierUrl: string): Response {
const location = res.headers.get("location");
if (res.status >= 300 && res.status < 400 && location) {
try {
const redirect = new URL(location, verifierUrl);
if (redirect.protocol === "http:" || redirect.protocol === "https:") {
return new Response(null, { status: res.status, headers: { location: redirect.href } });
}
} catch {
// Malformed or unsafe redirects fail closed below.
}
}
return new Response("Unauthorized", { status: res.status === 200 ? 401 : res.status });
}
/** Describe the original gateway request to a forward-auth verifier. */
export function forwardAuthHeaders(req: Request): Headers {
const original = new URL(req.url);
return new Headers({
cookie: req.headers.get("cookie") ?? "",
authorization: req.headers.get("authorization") ?? "",
"x-forwarded-host": req.headers.get("host") ?? original.host,
"x-forwarded-proto": original.protocol.replace(":", ""),
"x-original-method": req.method,
"x-original-uri": `${original.pathname}${original.search}`,
});
}
/** /**
* Enforce a per-app auth policy. Returns a Response to block, or null to allow. * Enforce a per-app auth policy. Returns a Response to block, or null to allow.
* `ip` is the client address (for the IP allowlist). * `ip` is the client address (for the IP allowlist).
@@ -140,16 +169,10 @@ async function checkAuth(
if (auth.forward) { if (auth.forward) {
try { try {
const res = await fetch(auth.forward.url, { const res = await fetch(auth.forward.url, {
headers: { headers: forwardAuthHeaders(req),
cookie: req.headers.get("cookie") ?? "",
authorization: req.headers.get("authorization") ?? "",
"x-forwarded-host": req.headers.get("host") ?? "",
"x-original-uri": new URL(req.url).pathname,
},
redirect: "manual", redirect: "manual",
}); });
if (!res.ok) if (!res.ok) return forwardAuthFailure(res, auth.forward.url);
return new Response("Unauthorized", { status: res.status === 200 ? 401 : res.status });
} catch { } catch {
return new Response("Auth service unavailable", { status: 503 }); return new Response("Auth service unavailable", { status: 503 });
} }
+40 -1
View File
@@ -1,5 +1,10 @@
import { expect, test } from "bun:test"; import { expect, test } from "bun:test";
import { defaultGatewayHostname, gatewayProxyHeaders } from "../src/gateway.ts"; import {
defaultGatewayHostname,
forwardAuthFailure,
forwardAuthHeaders,
gatewayProxyHeaders,
} from "../src/gateway.ts";
test("gateway uses platform-safe hostname defaults", () => { test("gateway uses platform-safe hostname defaults", () => {
expect(defaultGatewayHostname("development")).toBe("127.0.0.1"); expect(defaultGatewayHostname("development")).toBe("127.0.0.1");
@@ -17,3 +22,37 @@ test("gateway disables compression for its internal proxy hop", () => {
expect(headers.get("x-forwarded-proto")).toBe("http"); expect(headers.get("x-forwarded-proto")).toBe("http");
expect(headers.get("x-forwarded-for")).toBe("127.0.0.1"); expect(headers.get("x-forwarded-for")).toBe("127.0.0.1");
}); });
test("forward auth preserves intentional verifier redirects", () => {
const redirected = forwardAuthFailure(
new Response(null, { status: 302, headers: { location: "/login?returnTo=%2Fadmin" } }),
"http://sso.localhost:3000/api/verify",
);
const denied = forwardAuthFailure(new Response(null, { status: 401 }), "http://sso.localhost");
expect(redirected.status).toBe(302);
expect(redirected.headers.get("location")).toBe(
"http://sso.localhost:3000/login?returnTo=%2Fadmin",
);
expect(denied.status).toBe(401);
expect(denied.headers.has("location")).toBe(false);
});
test("forward auth describes the original gateway request", () => {
const headers = forwardAuthHeaders(
new Request("https://admin.example.test/settings?tab=security", {
headers: {
host: "admin.example.test",
cookie: "session=abc",
authorization: "Bearer token",
},
}),
);
expect(headers.get("x-forwarded-host")).toBe("admin.example.test");
expect(headers.get("x-forwarded-proto")).toBe("https");
expect(headers.get("x-original-method")).toBe("GET");
expect(headers.get("x-original-uri")).toBe("/settings?tab=security");
expect(headers.get("cookie")).toBe("session=abc");
expect(headers.get("authorization")).toBe("Bearer token");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+60
View File
@@ -0,0 +1,60 @@
# @wrnexus/helpers
Safe convenience helpers for common WrNexus application flows. The package uses
standard `Context`, `URL`, and `Response` values and has no runtime dependency beyond
`@wrnexus/core`.
## Installation
```bash
bun add @wrnexus/helpers
```
The package is private, so the machine must be authenticated to the `wrnexus` npm
organization.
## Forward-auth login redirects
The gateway calls an SSO verifier on a different URL from the original application.
These helpers reconstruct the original URL from the gateway headers and safely place it
in the login redirect:
```ts
import type { Context } from "@wrnexus/core";
import { redirectToLogin } from "@wrnexus/helpers";
export const GET = async (ctx: Context) => {
if (await hasValidSession(ctx)) {
return new Response(null, { status: 204 });
}
return redirectToLogin(ctx, "/login", {
allowedHosts: ["admin.localhost:3000", "reports.localhost:3000"],
});
};
```
This creates a response such as:
```text
Location: http://sso.localhost:3000/login?returnTo=http%3A%2F%2Fadmin.localhost%3A3000%2F
```
Always list the application hosts that are valid redirect destinations. Forwarded host
headers are rejected when `allowedHosts` is absent or does not match, preventing an open
redirect. A callback can support dynamic tenant domains:
```ts
allowedHosts: (host) => host.endsWith(".example.test");
```
## API
- `getOriginalRequestUrl(ctx, options): URL` — reconstruct the gateway URL.
- `getOriginalRequestOrigin(ctx, options): string` — return only its origin.
- `getOriginalRequestPath(ctx): string` — return its path and query string.
- `getOriginalRequestMethod(ctx): string` — return its HTTP method.
- `redirectToLogin(ctx, loginUrl, options): Response` — create a login redirect with an
encoded `returnTo` parameter.
For direct requests without gateway headers, URL helpers use `ctx.url`.
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@wrnexus/helpers",
"version": "0.2.17",
"private": true,
"type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"main": "src/index.ts",
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@wrnexus/core": "workspace:*"
}
}
+131
View File
@@ -0,0 +1,131 @@
/**
* @wrnexus/helpers safe conveniences for common WrNexus application flows.
*
* Helpers stay small and composable. They accept the standard WrNexus Context
* and return web-platform values such as URL and Response.
*/
import type { Context } from "@wrnexus/core";
export type RequestContext = Pick<Context, "req" | "url">;
export type AllowedHosts =
readonly string[] | ReadonlySet<string> | ((host: string, ctx: RequestContext) => boolean);
export interface OriginalRequestOptions {
/**
* Hosts that the application permits as redirect destinations. This is
* required when a proxy supplied X-Forwarded-Host is present.
*/
allowedHosts?: AllowedHosts;
}
export interface LoginRedirectOptions extends OriginalRequestOptions {
/** Query parameter that receives the original absolute URL. */
returnToParam?: string;
/** Browser redirect status. Defaults to 302. */
status?: 301 | 302 | 303 | 307 | 308;
}
function forwardedValue(ctx: RequestContext, name: string): string | null {
const value = ctx.req.headers.get(name)?.trim();
if (!value) return null;
if (value.includes(",")) {
throw new TypeError(`${name} must contain exactly one value`);
}
return value;
}
function hostIsAllowed(host: string, ctx: RequestContext, allowedHosts?: AllowedHosts): boolean {
if (!allowedHosts) return false;
if (typeof allowedHosts === "function") return allowedHosts(host, ctx);
const normalized = host.toLowerCase();
for (const allowed of allowedHosts) {
if (allowed.toLowerCase() === normalized) return true;
}
return false;
}
/** Get the original path and query string seen by the gateway. */
export function getOriginalRequestPath(ctx: RequestContext): string {
const value = forwardedValue(ctx, "x-original-uri") ?? `${ctx.url.pathname}${ctx.url.search}`;
if (
!value.startsWith("/") ||
value.startsWith("//") ||
value.includes("\\") ||
value.includes("#")
) {
throw new TypeError("x-original-uri must be an absolute request path");
}
return value;
}
/** Get the original HTTP method seen by the gateway. */
export function getOriginalRequestMethod(ctx: RequestContext): string {
const method = forwardedValue(ctx, "x-original-method") ?? ctx.req.method;
if (!/^[A-Za-z]+$/.test(method)) throw new TypeError("x-original-method is invalid");
return method.toUpperCase();
}
/**
* Reconstruct the absolute URL that reached the gateway.
*
* Forwarded hosts are never trusted implicitly: pass allowedHosts when this is
* used behind the WrNexus gateway. Direct requests fall back to ctx.url.
*/
export function getOriginalRequestUrl(
ctx: RequestContext,
options: OriginalRequestOptions = {},
): URL {
const host = forwardedValue(ctx, "x-forwarded-host");
const path = getOriginalRequestPath(ctx);
if (!host) return new URL(path, ctx.url.origin);
if (!hostIsAllowed(host, ctx, options.allowedHosts)) {
throw new TypeError(`Untrusted forwarded host: ${host}`);
}
const protocol = (forwardedValue(ctx, "x-forwarded-proto") ?? ctx.url.protocol).replace(/:$/, "");
if (protocol !== "http" && protocol !== "https") {
throw new TypeError(`Unsupported forwarded protocol: ${protocol}`);
}
const origin = new URL(`${protocol}://${host}`);
if (origin.username || origin.password || origin.pathname !== "/") {
throw new TypeError(`Invalid forwarded host: ${host}`);
}
const original = new URL(path, origin);
if (original.origin !== origin.origin) {
throw new TypeError("Original request URL must stay on the forwarded origin");
}
return original;
}
/** Get the original request origin, for example http://admin.localhost:3000. */
export function getOriginalRequestOrigin(
ctx: RequestContext,
options: OriginalRequestOptions = {},
): string {
return getOriginalRequestUrl(ctx, options).origin;
}
/**
* Redirect to a login page with the original absolute URL encoded as returnTo.
* Relative login URLs resolve against the current app (normally the SSO app).
*/
export function redirectToLogin(
ctx: RequestContext,
loginUrl: string | URL,
options: LoginRedirectOptions = {},
): Response {
const target = new URL(loginUrl, ctx.url.origin);
if (target.protocol !== "http:" && target.protocol !== "https:") {
throw new TypeError("Login URL must use http or https");
}
const original = getOriginalRequestUrl(ctx, options);
target.searchParams.set(options.returnToParam ?? "returnTo", original.href);
return Response.redirect(target, options.status ?? 302);
}
+90
View File
@@ -0,0 +1,90 @@
import { expect, test } from "bun:test";
import { createContext } from "@wrnexus/core";
import {
getOriginalRequestMethod,
getOriginalRequestOrigin,
getOriginalRequestPath,
getOriginalRequestUrl,
redirectToLogin,
} from "../src/index.ts";
function context(url: string, headers: HeadersInit = {}) {
const parsed = new URL(url);
return createContext(new Request(parsed, { headers }), parsed);
}
test("uses the direct context URL when no gateway headers exist", () => {
const ctx = context("https://app.example.test/account?tab=security");
expect(getOriginalRequestUrl(ctx).href).toBe("https://app.example.test/account?tab=security");
expect(getOriginalRequestOrigin(ctx)).toBe("https://app.example.test");
expect(getOriginalRequestPath(ctx)).toBe("/account?tab=security");
expect(getOriginalRequestMethod(ctx)).toBe("GET");
});
test("reconstructs an allowed original gateway URL", () => {
const ctx = context("http://sso.localhost:3000/api/verify", {
"x-forwarded-host": "admin.localhost:3000",
"x-forwarded-proto": "http",
"x-original-method": "GET",
"x-original-uri": "/users?page=2",
});
const url = getOriginalRequestUrl(ctx, { allowedHosts: ["admin.localhost:3000"] });
expect(url.href).toBe("http://admin.localhost:3000/users?page=2");
expect(getOriginalRequestMethod(ctx)).toBe("GET");
});
test("rejects untrusted hosts and unsafe request paths", () => {
const untrusted = context("http://sso.localhost/api/verify", {
"x-forwarded-host": "evil.example",
"x-original-uri": "/",
});
const unsafePath = context("http://sso.localhost/api/verify", {
"x-forwarded-host": "admin.localhost",
"x-original-uri": "//evil.example/steal",
});
expect(() => getOriginalRequestUrl(untrusted)).toThrow("Untrusted forwarded host");
expect(() => getOriginalRequestUrl(unsafePath, { allowedHosts: ["admin.localhost"] })).toThrow(
"absolute request path",
);
});
test("creates a safe login redirect with an encoded returnTo URL", () => {
const ctx = context("http://sso.localhost:3000/api/verify", {
"x-forwarded-host": "admin.localhost:3000",
"x-forwarded-proto": "http",
"x-original-uri": "/reports?range=week",
});
const response = redirectToLogin(ctx, "/login", {
allowedHosts: new Set(["admin.localhost:3000"]),
});
const location = new URL(response.headers.get("location")!);
expect(response.status).toBe(302);
expect(location.origin).toBe("http://sso.localhost:3000");
expect(location.pathname).toBe("/login");
expect(location.searchParams.get("returnTo")).toBe(
"http://admin.localhost:3000/reports?range=week",
);
});
test("supports an allowed-host callback and custom response options", () => {
const ctx = context("https://login.example.test/api/verify", {
"x-forwarded-host": "reports.example.test",
"x-forwarded-proto": "https",
"x-original-uri": "/",
});
const response = redirectToLogin(ctx, "https://login.example.test/sign-in?tenant=acme", {
allowedHosts: (host) => host.endsWith(".example.test"),
returnToParam: "next",
status: 303,
});
const location = new URL(response.headers.get("location")!);
expect(response.status).toBe(303);
expect(location.searchParams.get("tenant")).toBe("acme");
expect(location.searchParams.get("next")).toBe("https://reports.example.test/");
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.16", "version": "0.2.17",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.16", "version": "0.2.17",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1
View File
@@ -30,6 +30,7 @@
"@wrnexus/jwt": ["./packages/jwt/src/index.ts"], "@wrnexus/jwt": ["./packages/jwt/src/index.ts"],
"@wrnexus/oauth": ["./packages/oauth/src/index.ts"], "@wrnexus/oauth": ["./packages/oauth/src/index.ts"],
"@wrnexus/authz": ["./packages/authz/src/index.ts"], "@wrnexus/authz": ["./packages/authz/src/index.ts"],
"@wrnexus/helpers": ["./packages/helpers/src/index.ts"],
"@wrnexus/encryption": ["./packages/encryption/src/index.ts"], "@wrnexus/encryption": ["./packages/encryption/src/index.ts"],
"@wrnexus/pubsub": ["./packages/pubsub/src/index.ts"], "@wrnexus/pubsub": ["./packages/pubsub/src/index.ts"],
"@wrnexus/queue": ["./packages/queue/src/index.ts"], "@wrnexus/queue": ["./packages/queue/src/index.ts"],