Compare commits
102
Commits
cd0dffa87d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84e16ebc41 | ||
|
|
7993a303ac | ||
|
|
a99290a216 | ||
|
|
c648efc240 | ||
|
|
decbee2fd1 | ||
|
|
e62e7f5d27 | ||
|
|
b2c74a3a29 | ||
|
|
fe86570f45 | ||
|
|
bc1211ea8b | ||
|
|
d2e24eb089 | ||
|
|
e71885d51c | ||
|
|
138b5d0950 | ||
|
|
7e1eb899b3 | ||
|
|
3f6612cfe7 | ||
|
|
510b3ba777 | ||
|
|
1ff2c3339c | ||
|
|
027873459d | ||
|
|
d0bccfe37e | ||
|
|
120889060a | ||
|
|
26ec18c24b | ||
|
|
7658c5d499 | ||
|
|
3cb6ba1233 | ||
|
|
64db7da08d | ||
|
|
9f6552ea62 | ||
|
|
5bc391e74f | ||
|
|
3327ec0d37 | ||
|
|
ceb865f4f1 | ||
|
|
45309b3f72 | ||
|
|
8f3a13c854 | ||
|
|
eca4447dcf | ||
|
|
904bca21b2 | ||
|
|
b98d417185 | ||
|
|
7d737ec0a0 | ||
|
|
f5538ed621 | ||
|
|
d2d830e7e2 | ||
|
|
0618782355 | ||
|
|
9067e68f69 | ||
|
|
9d64ec60c4 | ||
|
|
fc1eea3939 | ||
|
|
4ec5dad60a | ||
|
|
a1a2947910 | ||
|
|
270040fb5f | ||
|
|
f9e285b182 | ||
|
|
3b402b4e36 | ||
|
|
fa535575c5 | ||
|
|
46ef4b775b | ||
|
|
7a36c1905f | ||
|
|
1a8c75a958 | ||
|
|
b3c93e9b18 | ||
|
|
613ff7ae5b | ||
|
|
e372ae571a | ||
|
|
354082ebc3 | ||
|
|
aa21bc4a18 | ||
|
|
1be482caab | ||
|
|
2d1cda0eab | ||
|
|
d26403fce2 | ||
|
|
f52e1d50e4 | ||
|
|
fe44bc2091 | ||
|
|
98e0813061 | ||
|
|
a9670c2a1c | ||
|
|
1a94179b5f | ||
|
|
f13e83fd74 | ||
|
|
7351ed1566 | ||
|
|
53b9947ef9 | ||
|
|
56cde5aaf8 | ||
|
|
4be4b2c346 | ||
|
|
6258495b67 | ||
|
|
631e1a2ddb | ||
|
|
2e12656060 | ||
|
|
d87b197224 | ||
|
|
64ab20cc95 | ||
|
|
46195462c3 | ||
|
|
8fb96f521f | ||
|
|
9377a69d88 | ||
|
|
9332522614 | ||
|
|
37439188ca | ||
|
|
7c5069b5b5 | ||
|
|
41b7eb8798 | ||
|
|
0be69fb234 | ||
|
|
bab11083c6 | ||
|
|
a3ddd39b7b | ||
|
|
96e082b943 | ||
|
|
2299a0726d | ||
|
|
2d0df4efc9 | ||
|
|
20783699ac | ||
|
|
d8305a5a14 | ||
|
|
98a46091b5 | ||
|
|
2c50a07ed2 | ||
|
|
9746e8e875 | ||
|
|
0ddb481159 | ||
|
|
3441b96362 | ||
|
|
84e58b1798 | ||
|
|
de4477dd9c | ||
|
|
2970d5fff3 | ||
|
|
eeef2d79df | ||
|
|
ef6de302e9 | ||
|
|
a970d04248 | ||
|
|
5429057a38 | ||
|
|
e944fd4496 | ||
|
|
861444b8a3 | ||
|
|
ba957e861e | ||
|
|
29febb2e0c |
@@ -13,6 +13,12 @@
|
|||||||
],
|
],
|
||||||
"port": 3520
|
"port": 3520
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sendline",
|
||||||
|
"runtimeExecutable": "bun",
|
||||||
|
"runtimeArgs": ["run", "--cwd", "D:/Company/sendline", "dev", "--port=3610"],
|
||||||
|
"port": 3610
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "component-showcase",
|
"name": "component-showcase",
|
||||||
"runtimeExecutable": "bun",
|
"runtimeExecutable": "bun",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.3.14
|
bun-version: 1.4.0
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 24
|
node-version: 24
|
||||||
|
|||||||
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Added framework-owned authentication and authorization integration: lazy auth stores,
|
||||||
|
configured OAuth routes with cookie-bound transactions, production configuration validation,
|
||||||
|
default lifecycle roles, typed request users, declarative API permissions, page guards, and
|
||||||
|
consistent unauthorized/forbidden responses.
|
||||||
|
- Added reusable application primitives for typed route params, bounded cursor pagination,
|
||||||
|
owned-resource authorization, atomic database state transitions, queue batching and explicit
|
||||||
|
worker lifecycles.
|
||||||
|
- Expanded `@wrnexus/test` with official typed contexts, Bun-compatible fetch mocking and a
|
||||||
|
full-stack harness alias; made generated files deterministic and introduced `wrnexus check` as
|
||||||
|
the canonical build-before-check command for generated applications.
|
||||||
|
|
||||||
- Fixed `v.boolean()` coercion in `@wrnexus/validation` (`checkField` in both `src/index.ts`
|
- Fixed `v.boolean()` coercion in `@wrnexus/validation` (`checkField` in both `src/index.ts`
|
||||||
and the browser mirror in `src/runtime.ts`): previously any string other than `"true"` or
|
and the browser mirror in `src/runtime.ts`): previously any string other than `"true"` or
|
||||||
`"on"` silently coerced to `false` with no error, so typos and unrecognised values (e.g.
|
`"on"` silently coerced to `false` with no error, so typos and unrecognised values (e.g.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.4.0",
|
||||||
"@types/react": "^19.2.18",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.4",
|
"@types/react-dom": "^19.2.4",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@wrnexus/cli": "^0.8.35",
|
"@wrnexus/cli": "^0.8.36",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -145,27 +145,27 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/ai": "^0.8.9",
|
"@wrnexus/ai": "^0.8.9",
|
||||||
"@wrnexus/auth": "^0.8.11",
|
"@wrnexus/auth": "^0.8.12",
|
||||||
"@wrnexus/authz": "^0.8.9",
|
"@wrnexus/authz": "^0.8.9",
|
||||||
"@wrnexus/captcha": "^0.8.10",
|
"@wrnexus/captcha": "^0.8.11",
|
||||||
"@wrnexus/core": "^0.8.9",
|
"@wrnexus/core": "^0.8.9",
|
||||||
"@wrnexus/csr": "^0.8.21",
|
"@wrnexus/csr": "^0.8.21",
|
||||||
"@wrnexus/db": "^0.8.14",
|
"@wrnexus/db": "^0.8.15",
|
||||||
"@wrnexus/dev-server": "^0.8.32",
|
"@wrnexus/dev-server": "^0.8.33",
|
||||||
"@wrnexus/encryption": "^0.8.8",
|
"@wrnexus/encryption": "^0.8.9",
|
||||||
"@wrnexus/helpers": "^0.8.8",
|
"@wrnexus/helpers": "^0.8.8",
|
||||||
"@wrnexus/i18n": "^0.8.10",
|
"@wrnexus/i18n": "^0.8.11",
|
||||||
"@wrnexus/image": "^0.8.9",
|
"@wrnexus/image": "^0.8.10",
|
||||||
"@wrnexus/jwt": "^0.8.8",
|
"@wrnexus/jwt": "^0.8.9",
|
||||||
"@wrnexus/observability": "^0.8.8",
|
"@wrnexus/observability": "^0.8.8",
|
||||||
"@wrnexus/realtime": "^0.8.9",
|
"@wrnexus/realtime": "^0.8.10",
|
||||||
"@wrnexus/security": "^0.8.8",
|
"@wrnexus/security": "^0.8.8",
|
||||||
"@wrnexus/store": "^0.8.8",
|
"@wrnexus/store": "^0.8.8",
|
||||||
"@wrnexus/styles": "^0.8.15",
|
"@wrnexus/styles": "^0.8.15",
|
||||||
"@wrnexus/tracking": "^0.8.8",
|
"@wrnexus/tracking": "^0.8.8",
|
||||||
"@wrnexus/ui": "^0.8.19",
|
"@wrnexus/ui": "^0.8.19",
|
||||||
"@wrnexus/uploader": "^0.8.9",
|
"@wrnexus/uploader": "^0.8.10",
|
||||||
"@wrnexus/validation": "^0.8.9",
|
"@wrnexus/validation": "^0.8.10",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -173,7 +173,7 @@
|
|||||||
"@iconify/tailwind4": "^1.2.3",
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
"@tailwindcss/cli": "^4.3.3",
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@wrnexus/cli": "^0.8.35",
|
"@wrnexus/cli": "^0.8.36",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
@@ -186,27 +186,27 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/ai": "^0.8.9",
|
"@wrnexus/ai": "^0.8.9",
|
||||||
"@wrnexus/auth": "^0.8.11",
|
"@wrnexus/auth": "^0.8.12",
|
||||||
"@wrnexus/authz": "^0.8.9",
|
"@wrnexus/authz": "^0.8.9",
|
||||||
"@wrnexus/captcha": "^0.8.10",
|
"@wrnexus/captcha": "^0.8.11",
|
||||||
"@wrnexus/core": "^0.8.9",
|
"@wrnexus/core": "^0.8.9",
|
||||||
"@wrnexus/csr": "^0.8.21",
|
"@wrnexus/csr": "^0.8.21",
|
||||||
"@wrnexus/db": "^0.8.14",
|
"@wrnexus/db": "^0.8.15",
|
||||||
"@wrnexus/dev-server": "^0.8.32",
|
"@wrnexus/dev-server": "^0.8.33",
|
||||||
"@wrnexus/encryption": "^0.8.8",
|
"@wrnexus/encryption": "^0.8.9",
|
||||||
"@wrnexus/helpers": "^0.8.8",
|
"@wrnexus/helpers": "^0.8.8",
|
||||||
"@wrnexus/i18n": "^0.8.10",
|
"@wrnexus/i18n": "^0.8.11",
|
||||||
"@wrnexus/image": "^0.8.9",
|
"@wrnexus/image": "^0.8.10",
|
||||||
"@wrnexus/jwt": "^0.8.8",
|
"@wrnexus/jwt": "^0.8.9",
|
||||||
"@wrnexus/observability": "^0.8.8",
|
"@wrnexus/observability": "^0.8.8",
|
||||||
"@wrnexus/realtime": "^0.8.9",
|
"@wrnexus/realtime": "^0.8.10",
|
||||||
"@wrnexus/security": "^0.8.8",
|
"@wrnexus/security": "^0.8.8",
|
||||||
"@wrnexus/store": "^0.8.8",
|
"@wrnexus/store": "^0.8.8",
|
||||||
"@wrnexus/styles": "^0.8.15",
|
"@wrnexus/styles": "^0.8.15",
|
||||||
"@wrnexus/tracking": "^0.8.8",
|
"@wrnexus/tracking": "^0.8.8",
|
||||||
"@wrnexus/ui": "^0.8.19",
|
"@wrnexus/ui": "^0.8.19",
|
||||||
"@wrnexus/uploader": "^0.8.9",
|
"@wrnexus/uploader": "^0.8.10",
|
||||||
"@wrnexus/validation": "^0.8.9",
|
"@wrnexus/validation": "^0.8.10",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
"@iconify/tailwind4": "^1.2.3",
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
"@tailwindcss/cli": "^4.3.3",
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"@wrnexus/cli": "^0.8.35",
|
"@wrnexus/cli": "^0.8.36",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
"prettier": "^3.9.6",
|
"prettier": "^3.9.6",
|
||||||
"tailwindcss": "^4.3.3",
|
"tailwindcss": "^4.3.3",
|
||||||
@@ -235,7 +235,7 @@
|
|||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.13",
|
"version": "0.8.18",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -257,10 +257,11 @@
|
|||||||
},
|
},
|
||||||
"packages/authz": {
|
"packages/authz": {
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.8.9",
|
"version": "0.8.15",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/db": "workspace:*",
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/plugin": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/benchmark": {
|
"packages/benchmark": {
|
||||||
@@ -276,7 +277,7 @@
|
|||||||
},
|
},
|
||||||
"packages/captcha": {
|
"packages/captcha": {
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.11",
|
"version": "0.8.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -291,7 +292,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.47",
|
"version": "0.8.64",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -319,7 +320,7 @@
|
|||||||
},
|
},
|
||||||
"packages/compiler": {
|
"packages/compiler": {
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.14",
|
"version": "0.8.22",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/csr": "workspace:*",
|
"@wrnexus/csr": "workspace:*",
|
||||||
"@wrnexus/store": "workspace:*",
|
"@wrnexus/store": "workspace:*",
|
||||||
@@ -337,18 +338,18 @@
|
|||||||
},
|
},
|
||||||
"packages/core": {
|
"packages/core": {
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.8.10",
|
"version": "0.8.12",
|
||||||
},
|
},
|
||||||
"packages/csr": {
|
"packages/csr": {
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.25",
|
"version": "0.8.35",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/db": {
|
"packages/db": {
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.16",
|
"version": "0.8.20",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -356,7 +357,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.42",
|
"version": "0.8.56",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
@@ -370,6 +371,8 @@
|
|||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
"@wrnexus/pubsub": "workspace:*",
|
"@wrnexus/pubsub": "workspace:*",
|
||||||
"@wrnexus/pwa": "workspace:*",
|
"@wrnexus/pwa": "workspace:*",
|
||||||
|
"@wrnexus/queue": "workspace:*",
|
||||||
|
"@wrnexus/react": "workspace:*",
|
||||||
"@wrnexus/router": "workspace:*",
|
"@wrnexus/router": "workspace:*",
|
||||||
"@wrnexus/rpc": "workspace:*",
|
"@wrnexus/rpc": "workspace:*",
|
||||||
"@wrnexus/security": "workspace:*",
|
"@wrnexus/security": "workspace:*",
|
||||||
@@ -383,7 +386,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-toolbar": {
|
"packages/dev-toolbar": {
|
||||||
"name": "@wrnexus/dev-toolbar",
|
"name": "@wrnexus/dev-toolbar",
|
||||||
"version": "0.8.13",
|
"version": "0.8.16",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -391,9 +394,10 @@
|
|||||||
},
|
},
|
||||||
"packages/encryption": {
|
"packages/encryption": {
|
||||||
"name": "@wrnexus/encryption",
|
"name": "@wrnexus/encryption",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.3.14",
|
||||||
@@ -409,9 +413,11 @@
|
|||||||
},
|
},
|
||||||
"packages/helpers": {
|
"packages/helpers": {
|
||||||
"name": "@wrnexus/helpers",
|
"name": "@wrnexus/helpers",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/i18n": {
|
"packages/i18n": {
|
||||||
@@ -470,7 +476,7 @@
|
|||||||
},
|
},
|
||||||
"packages/language-server": {
|
"packages/language-server": {
|
||||||
"name": "@wrnexus/language-server",
|
"name": "@wrnexus/language-server",
|
||||||
"version": "0.8.11",
|
"version": "0.8.12",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus-language-server": "src/server.ts",
|
"wrnexus-language-server": "src/server.ts",
|
||||||
},
|
},
|
||||||
@@ -480,6 +486,14 @@
|
|||||||
"vscode-html-languageservice": "^5.6.2",
|
"vscode-html-languageservice": "^5.6.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/mail": {
|
||||||
|
"name": "@wrnexus/mail",
|
||||||
|
"version": "0.8.3",
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/encryption": "workspace:*",
|
||||||
|
"@wrnexus/queue": "workspace:*",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/mcp": {
|
"packages/mcp": {
|
||||||
"name": "@wrnexus/mcp",
|
"name": "@wrnexus/mcp",
|
||||||
"version": "0.8.8",
|
"version": "0.8.8",
|
||||||
@@ -487,6 +501,10 @@
|
|||||||
"wrnexus-mcp": "src/stdio.ts",
|
"wrnexus-mcp": "src/stdio.ts",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/metering": {
|
||||||
|
"name": "@wrnexus/metering",
|
||||||
|
"version": "0.8.3",
|
||||||
|
},
|
||||||
"packages/mobile": {
|
"packages/mobile": {
|
||||||
"name": "@wrnexus/mobile",
|
"name": "@wrnexus/mobile",
|
||||||
"version": "0.8.8",
|
"version": "0.8.8",
|
||||||
@@ -512,6 +530,14 @@
|
|||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/payment": {
|
||||||
|
"name": "@wrnexus/payment",
|
||||||
|
"version": "0.8.1",
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/plugin": "workspace:*",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/playground": {
|
"packages/playground": {
|
||||||
"name": "@wrnexus/playground",
|
"name": "@wrnexus/playground",
|
||||||
"version": "0.8.8",
|
"version": "0.8.8",
|
||||||
@@ -545,9 +571,10 @@
|
|||||||
},
|
},
|
||||||
"packages/queue": {
|
"packages/queue": {
|
||||||
"name": "@wrnexus/queue",
|
"name": "@wrnexus/queue",
|
||||||
"version": "0.8.8",
|
"version": "0.8.14",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
"@wrnexus/rpc": "workspace:*",
|
"@wrnexus/rpc": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -572,7 +599,7 @@
|
|||||||
},
|
},
|
||||||
"packages/reactive": {
|
"packages/reactive": {
|
||||||
"name": "@wrnexus/reactive",
|
"name": "@wrnexus/reactive",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
},
|
},
|
||||||
"packages/realtime": {
|
"packages/realtime": {
|
||||||
"name": "@wrnexus/realtime",
|
"name": "@wrnexus/realtime",
|
||||||
@@ -590,7 +617,7 @@
|
|||||||
},
|
},
|
||||||
"packages/router": {
|
"packages/router": {
|
||||||
"name": "@wrnexus/router",
|
"name": "@wrnexus/router",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/compiler": "workspace:*",
|
"@wrnexus/compiler": "workspace:*",
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
@@ -612,7 +639,7 @@
|
|||||||
},
|
},
|
||||||
"packages/security": {
|
"packages/security": {
|
||||||
"name": "@wrnexus/security",
|
"name": "@wrnexus/security",
|
||||||
"version": "0.8.8",
|
"version": "0.8.9",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -628,11 +655,11 @@
|
|||||||
},
|
},
|
||||||
"packages/store": {
|
"packages/store": {
|
||||||
"name": "@wrnexus/store",
|
"name": "@wrnexus/store",
|
||||||
"version": "0.8.8",
|
"version": "0.8.12",
|
||||||
},
|
},
|
||||||
"packages/styles": {
|
"packages/styles": {
|
||||||
"name": "@wrnexus/styles",
|
"name": "@wrnexus/styles",
|
||||||
"version": "0.8.15",
|
"version": "0.8.20",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -641,11 +668,11 @@
|
|||||||
},
|
},
|
||||||
"packages/syntax": {
|
"packages/syntax": {
|
||||||
"name": "@wrnexus/syntax",
|
"name": "@wrnexus/syntax",
|
||||||
"version": "0.8.10",
|
"version": "0.8.18",
|
||||||
},
|
},
|
||||||
"packages/test": {
|
"packages/test": {
|
||||||
"name": "@wrnexus/test",
|
"name": "@wrnexus/test",
|
||||||
"version": "0.8.9",
|
"version": "0.8.12",
|
||||||
},
|
},
|
||||||
"packages/tracking": {
|
"packages/tracking": {
|
||||||
"name": "@wrnexus/tracking",
|
"name": "@wrnexus/tracking",
|
||||||
@@ -653,7 +680,7 @@
|
|||||||
},
|
},
|
||||||
"packages/typecheck": {
|
"packages/typecheck": {
|
||||||
"name": "@wrnexus/typecheck",
|
"name": "@wrnexus/typecheck",
|
||||||
"version": "0.8.10",
|
"version": "0.8.16",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/syntax": "workspace:*",
|
"@wrnexus/syntax": "workspace:*",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -661,14 +688,14 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.21",
|
"version": "0.8.56",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages/uploader": {
|
"packages/uploader": {
|
||||||
"name": "@wrnexus/uploader",
|
"name": "@wrnexus/uploader",
|
||||||
"version": "0.8.10",
|
"version": "0.8.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/plugin": "workspace:*",
|
"@wrnexus/plugin": "workspace:*",
|
||||||
@@ -769,7 +796,7 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="],
|
||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
@@ -795,7 +822,7 @@
|
|||||||
|
|
||||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
|
||||||
|
|
||||||
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.123", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-0CozmpKXEOEEhltrfT2zt+/t1hez67Y+hM2VJWoIcBKdBrb83VYuKf+b5A0QU9HfQm+RNn2GjFWlTOwi+Ss6IA=="],
|
"@iconify-json/lucide": ["@iconify-json/lucide@1.2.125", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-tOCk1QKMtKnCfPAgZRHgjRkQTP7wF5IO+iPKvvp8vxGZYPkSLhx4HTV3Ng0pIZ3wNWrS6kVpHkunJ1dc19L1og=="],
|
||||||
|
|
||||||
"@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="],
|
"@iconify/tailwind4": ["@iconify/tailwind4@1.2.3", "", { "dependencies": { "@iconify/tools": "^5.0.5", "@iconify/types": "^2.0.0", "@iconify/utils": "^3.1.0" }, "peerDependencies": { "tailwindcss": ">= 4.0.0" } }, "sha512-z8SKiMHRASJKF/IY//87MF88lcB7ulxh8vlhQXXLWsBkNtOh6ese9R41MyGpQeqXdRvQVt+/fX2glQtHFjQ+MA=="],
|
||||||
|
|
||||||
@@ -815,6 +842,8 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||||
|
|
||||||
|
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||||
|
|
||||||
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
"@noble/hashes": ["@noble/hashes@1.4.0", "", {}, "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg=="],
|
||||||
|
|
||||||
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
"@parcel/watcher": ["@parcel/watcher@2.5.1", "", { "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", "micromatch": "^4.0.5", "node-addon-api": "^7.0.0" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.1", "@parcel/watcher-darwin-arm64": "2.5.1", "@parcel/watcher-darwin-x64": "2.5.1", "@parcel/watcher-freebsd-x64": "2.5.1", "@parcel/watcher-linux-arm-glibc": "2.5.1", "@parcel/watcher-linux-arm-musl": "2.5.1", "@parcel/watcher-linux-arm64-glibc": "2.5.1", "@parcel/watcher-linux-arm64-musl": "2.5.1", "@parcel/watcher-linux-x64-glibc": "2.5.1", "@parcel/watcher-linux-x64-musl": "2.5.1", "@parcel/watcher-win32-arm64": "2.5.1", "@parcel/watcher-win32-ia32": "2.5.1", "@parcel/watcher-win32-x64": "2.5.1" } }, "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg=="],
|
||||||
@@ -845,79 +874,79 @@
|
|||||||
|
|
||||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
|
||||||
|
|
||||||
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-NgekZOrSJFSBFLFoLfwePguAWAx7z1+f2TEsWFUMyiqqfntZ4+S/S5hzqME3q4pCA0iOsFKdwiQ35dwY24eVqA=="],
|
"@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "@peculiar/asn1-x509-attr": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw=="],
|
||||||
|
|
||||||
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-akbF8+uvleHs8sejNPQxwmVFuInAg6FMNHOwMILXfP518YfFJwdR3jr6oNUPOaEJfuEhn/vkNOCIT6ASUd4mbg=="],
|
"@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw=="],
|
||||||
|
|
||||||
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ohwlk+u9Rv2NOAY1c6MfHj45ATVF8R1DUN/WCgABiRtLi2ZftlZWZX7KvpAbU8v9xPcmoILfELeEABj/rn18AQ=="],
|
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ=="],
|
||||||
|
|
||||||
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-rsa": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-5yof1ytoB++RQtaFbqSUJ8pxDJtZT6vbVqZ8XoJ61ph7UjNVvfFwAilnCodqkNsAodpy13gDhoxZXw00pghnyg=="],
|
"@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.9.4", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.4", "@peculiar/asn1-pkcs8": "^2.9.4", "@peculiar/asn1-rsa": "^2.9.4", "@peculiar/asn1-schema": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw=="],
|
||||||
|
|
||||||
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-qAKXtLpBEw9LqhKpjw3ajZSXlBur+ipW+y2ivVBQAG6F6qRx94yO+1ZR4mvw+YaCfKSaOzLeYEzsPaBp4SJELA=="],
|
"@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ=="],
|
||||||
|
|
||||||
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.8.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.8.0", "@peculiar/asn1-pfx": "^2.8.0", "@peculiar/asn1-pkcs8": "^2.8.0", "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "@peculiar/asn1-x509-attr": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-b5nDWCnkV60+cQ141D6sVVwK9nz64R5n3zSVnklGd+ECdkW2Ol3U1a6yYFlalpSOaD557yuJB64A+q42jG7lUQ=="],
|
"@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.9.4", "", { "dependencies": { "@peculiar/asn1-cms": "^2.9.4", "@peculiar/asn1-pfx": "^2.9.4", "@peculiar/asn1-pkcs8": "^2.9.4", "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "@peculiar/asn1-x509-attr": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ=="],
|
||||||
|
|
||||||
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-zHEUlCqB2mk7x2lxDwHHJy7hWZOPdGHVlsmITWKB5/PbQo61atbu9PJ/0r9dQNMwFzbKPXZ8uK8/91eUhRznSg=="],
|
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA=="],
|
||||||
|
|
||||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="],
|
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.9.4", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg=="],
|
||||||
|
|
||||||
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-N0CMuhWUzsWEVq6F1q9X6+VKUnWzSW+cSVg+aPaGGwDdbFoFWTYgin5MHwXgpWd6y9COMBxnfy/Qc+Xc7F0Zwg=="],
|
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q=="],
|
||||||
|
|
||||||
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.8.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.8.0", "@peculiar/asn1-x509": "^2.8.0", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-tHjkfS/qhMnmrlB2J9NhflQlQ7In3khO3CfmVrriOlpTeErY9ZIKOso1hQ5JQiyrJ7ShvqVPk7E5fQmbclkSKA=="],
|
"@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.9.4", "", { "dependencies": { "@peculiar/asn1-schema": "^2.9.4", "@peculiar/asn1-x509": "^2.9.4", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg=="],
|
||||||
|
|
||||||
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
|
"@peculiar/utils": ["@peculiar/utils@2.0.3", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ=="],
|
||||||
|
|
||||||
"@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="],
|
"@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.2", "", { "os": "android", "cpu": "arm" }, "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.62.5", "", { "os": "android", "cpu": "arm" }, "sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw=="],
|
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.62.5", "", { "os": "android", "cpu": "arm64" }, "sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A=="],
|
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.62.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A=="],
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA=="],
|
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.62.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw=="],
|
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.62.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg=="],
|
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.62.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg=="],
|
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.2", "", { "os": "linux", "cpu": "arm" }, "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA=="],
|
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.62.5", "", { "os": "linux", "cpu": "arm" }, "sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA=="],
|
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ=="],
|
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.62.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg=="],
|
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ=="],
|
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A=="],
|
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w=="],
|
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.62.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg=="],
|
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.2", "", { "os": "linux", "cpu": "none" }, "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q=="],
|
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.62.5", "", { "os": "linux", "cpu": "none" }, "sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg=="],
|
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.62.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A=="],
|
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA=="],
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg=="],
|
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.62.5", "", { "os": "linux", "cpu": "x64" }, "sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw=="],
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg=="],
|
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.62.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw=="],
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.2", "", { "os": "none", "cpu": "arm64" }, "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA=="],
|
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.62.5", "", { "os": "none", "cpu": "arm64" }, "sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg=="],
|
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.62.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q=="],
|
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.62.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg=="],
|
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.2", "", { "os": "win32", "cpu": "x64" }, "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA=="],
|
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.62.5", "", { "os": "win32", "cpu": "x64" }, "sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg=="],
|
||||||
|
|
||||||
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
|
"@tailwindcss/cli": ["@tailwindcss/cli@4.3.3", "", { "dependencies": { "@parcel/watcher": "2.5.1", "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "enhanced-resolve": "^5.24.1", "mri": "^1.2.0", "picocolors": "^1.1.1", "tailwindcss": "4.3.3" }, "bin": { "tailwindcss": "./dist/index.mjs" } }, "sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw=="],
|
||||||
|
|
||||||
@@ -949,7 +978,7 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
"@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="],
|
||||||
|
|
||||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||||
|
|
||||||
@@ -957,11 +986,11 @@
|
|||||||
|
|
||||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="],
|
"@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.4", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw=="],
|
"@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="],
|
||||||
|
|
||||||
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
"@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="],
|
||||||
|
|
||||||
@@ -1033,10 +1062,14 @@
|
|||||||
|
|
||||||
"@wrnexus/language-server": ["@wrnexus/language-server@workspace:packages/language-server"],
|
"@wrnexus/language-server": ["@wrnexus/language-server@workspace:packages/language-server"],
|
||||||
|
|
||||||
|
"@wrnexus/mail": ["@wrnexus/mail@workspace:packages/mail"],
|
||||||
|
|
||||||
"@wrnexus/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"],
|
"@wrnexus/managed-captcha-service": ["@wrnexus/managed-captcha-service@workspace:services/managed-captcha"],
|
||||||
|
|
||||||
"@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"],
|
"@wrnexus/mcp": ["@wrnexus/mcp@workspace:packages/mcp"],
|
||||||
|
|
||||||
|
"@wrnexus/metering": ["@wrnexus/metering@workspace:packages/metering"],
|
||||||
|
|
||||||
"@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"],
|
"@wrnexus/mobile": ["@wrnexus/mobile@workspace:packages/mobile"],
|
||||||
|
|
||||||
"@wrnexus/native": ["@wrnexus/native@workspace:packages/native"],
|
"@wrnexus/native": ["@wrnexus/native@workspace:packages/native"],
|
||||||
@@ -1045,6 +1078,8 @@
|
|||||||
|
|
||||||
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
|
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
|
||||||
|
|
||||||
|
"@wrnexus/payment": ["@wrnexus/payment@workspace:packages/payment"],
|
||||||
|
|
||||||
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
|
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
|
||||||
|
|
||||||
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
|
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
|
||||||
@@ -1087,7 +1122,7 @@
|
|||||||
|
|
||||||
"@wrnexus/validation": ["@wrnexus/validation@workspace:packages/validation"],
|
"@wrnexus/validation": ["@wrnexus/validation@workspace:packages/validation"],
|
||||||
|
|
||||||
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
@@ -1113,7 +1148,7 @@
|
|||||||
|
|
||||||
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
|
"buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
"bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="],
|
||||||
|
|
||||||
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
|
||||||
|
|
||||||
@@ -1145,7 +1180,7 @@
|
|||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
@@ -1167,7 +1202,7 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
"eslint": ["eslint@10.8.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ=="],
|
"eslint": ["eslint@10.9.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw=="],
|
||||||
|
|
||||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||||
|
|
||||||
@@ -1203,7 +1238,7 @@
|
|||||||
|
|
||||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||||
|
|
||||||
"flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
|
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
@@ -1211,7 +1246,7 @@
|
|||||||
|
|
||||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
"happy-dom": ["happy-dom@20.11.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw=="],
|
"happy-dom": ["happy-dom@20.11.6", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-Hldbg8AdAa5a5oDcZpjqnGitp7JB0hqWmfv/8qr+kft4vzSD8BHsbdRfzYvL/0QcbKcURC/yyoygbeDQarPvYg=="],
|
||||||
|
|
||||||
"i18n-showcase": ["i18n-showcase@workspace:examples/i18n-showcase"],
|
"i18n-showcase": ["i18n-showcase@workspace:examples/i18n-showcase"],
|
||||||
|
|
||||||
@@ -1283,7 +1318,7 @@
|
|||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
"minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="],
|
||||||
|
|
||||||
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
"mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="],
|
||||||
|
|
||||||
@@ -1319,7 +1354,7 @@
|
|||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
"pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="],
|
||||||
|
|
||||||
@@ -1337,7 +1372,7 @@
|
|||||||
|
|
||||||
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
|
|
||||||
"pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="],
|
"pvutils": ["pvutils@1.2.0", "", {}, "sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg=="],
|
||||||
|
|
||||||
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
"react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="],
|
||||||
|
|
||||||
@@ -1349,9 +1384,9 @@
|
|||||||
|
|
||||||
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
|
||||||
|
|
||||||
"rollup": ["rollup@4.62.2", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.62.2", "@rollup/rollup-android-arm64": "4.62.2", "@rollup/rollup-darwin-arm64": "4.62.2", "@rollup/rollup-darwin-x64": "4.62.2", "@rollup/rollup-freebsd-arm64": "4.62.2", "@rollup/rollup-freebsd-x64": "4.62.2", "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", "@rollup/rollup-linux-arm-musleabihf": "4.62.2", "@rollup/rollup-linux-arm64-gnu": "4.62.2", "@rollup/rollup-linux-arm64-musl": "4.62.2", "@rollup/rollup-linux-loong64-gnu": "4.62.2", "@rollup/rollup-linux-loong64-musl": "4.62.2", "@rollup/rollup-linux-ppc64-gnu": "4.62.2", "@rollup/rollup-linux-ppc64-musl": "4.62.2", "@rollup/rollup-linux-riscv64-gnu": "4.62.2", "@rollup/rollup-linux-riscv64-musl": "4.62.2", "@rollup/rollup-linux-s390x-gnu": "4.62.2", "@rollup/rollup-linux-x64-gnu": "4.62.2", "@rollup/rollup-linux-x64-musl": "4.62.2", "@rollup/rollup-openbsd-x64": "4.62.2", "@rollup/rollup-openharmony-arm64": "4.62.2", "@rollup/rollup-win32-arm64-msvc": "4.62.2", "@rollup/rollup-win32-ia32-msvc": "4.62.2", "@rollup/rollup-win32-x64-gnu": "4.62.2", "@rollup/rollup-win32-x64-msvc": "4.62.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA=="],
|
"rollup": ["rollup@4.62.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.62.5", "@rollup/rollup-android-arm64": "4.62.5", "@rollup/rollup-darwin-arm64": "4.62.5", "@rollup/rollup-darwin-x64": "4.62.5", "@rollup/rollup-freebsd-arm64": "4.62.5", "@rollup/rollup-freebsd-x64": "4.62.5", "@rollup/rollup-linux-arm-gnueabihf": "4.62.5", "@rollup/rollup-linux-arm-musleabihf": "4.62.5", "@rollup/rollup-linux-arm64-gnu": "4.62.5", "@rollup/rollup-linux-arm64-musl": "4.62.5", "@rollup/rollup-linux-loong64-gnu": "4.62.5", "@rollup/rollup-linux-loong64-musl": "4.62.5", "@rollup/rollup-linux-ppc64-gnu": "4.62.5", "@rollup/rollup-linux-ppc64-musl": "4.62.5", "@rollup/rollup-linux-riscv64-gnu": "4.62.5", "@rollup/rollup-linux-riscv64-musl": "4.62.5", "@rollup/rollup-linux-s390x-gnu": "4.62.5", "@rollup/rollup-linux-x64-gnu": "4.62.5", "@rollup/rollup-linux-x64-musl": "4.62.5", "@rollup/rollup-openbsd-x64": "4.62.5", "@rollup/rollup-openharmony-arm64": "4.62.5", "@rollup/rollup-win32-arm64-msvc": "4.62.5", "@rollup/rollup-win32-ia32-msvc": "4.62.5", "@rollup/rollup-win32-x64-gnu": "4.62.5", "@rollup/rollup-win32-x64-msvc": "4.62.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw=="],
|
||||||
|
|
||||||
"sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
"sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||||
|
|
||||||
@@ -1427,27 +1462,27 @@
|
|||||||
|
|
||||||
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
|
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
|
||||||
|
|
||||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
"@antfu/install-pkg/tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="],
|
"@antfu/install-pkg/tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" }, "bundled": true }, "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="],
|
||||||
|
|
||||||
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Application productivity kit
|
||||||
|
|
||||||
|
WrNexus applications should declare infrastructure once and keep only domain decisions locally.
|
||||||
|
|
||||||
|
## Included foundations
|
||||||
|
|
||||||
|
1. HTTP routes: `defineApiRoute`, `authorized`, `requireUser`, `requirePermission`, `requireParam`, `requireJson`, and `json`.
|
||||||
|
2. CRUD resources: `defineResource` generates owned list/create/get/update/delete handlers.
|
||||||
|
3. Seed data: `addSeedData`, `upsertSeedData`, `seedIfMissing`, `defineSeed`, `removeSeedData`, and `runSeedQuery`.
|
||||||
|
4. Transactional delivery: `withOutbox`, `drainOutbox`, and `installOutboxSchema`.
|
||||||
|
5. Job state transitions: `defineJobStateMachine`.
|
||||||
|
6. SQLite/PostgreSQL/MySQL queue tables: configured storage plus `databaseQueueStore`.
|
||||||
|
7. Queue operations: success/failed hooks, dashboard handler and queue CLI.
|
||||||
|
8. Queue testing: `testQueue` with deterministic `runNext`/`runAll`.
|
||||||
|
9. Workflows and compensation: durable workflows and `defineSaga`.
|
||||||
|
10. Mail: `defineMail`, `defineMailTemplate`, sandbox allowlists and `queuedMail`.
|
||||||
|
11. Encrypted settings: `defineSecretResource`.
|
||||||
|
12. Ledgers: `defineLedger` with atomic debit and audit entries.
|
||||||
|
13. Forms: `useForm` for values, errors, dirty/submitting state, validation and submission.
|
||||||
|
14. Typed HTTP calls: `createApiClient`; generated OpenAPI/SDK clients remain available through `wrnexus api generate`.
|
||||||
|
15. Full-stack testing: `createTestApp`, database rollback/factories, and queue test controls.
|
||||||
|
16. Package migrations: queue/auth/authz packages contribute migrations through the plugin lifecycle.
|
||||||
|
17. Auth/OAuth: auth and authz plugins own routes, middleware, roles and provider lifecycle.
|
||||||
|
18. Code generation: `wrnexus generate resource <name>`.
|
||||||
|
19. Incremental features: `wrnexus add queue|mail|seed|testing|resource`.
|
||||||
|
20. Starter presets: `wrnexus create app --preset=saas|email` and `--features=...`.
|
||||||
|
|
||||||
|
## Recommended new-project flow
|
||||||
|
|
||||||
|
```sh
|
||||||
|
wrnexus create acme --preset=saas
|
||||||
|
cd acme && bun install
|
||||||
|
wrnexus generate resource projects
|
||||||
|
wrnexus add mail
|
||||||
|
wrnexus db migrate
|
||||||
|
wrnexus db seed
|
||||||
|
wrnexus dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Use generated resources for ordinary owned CRUD. Use the outbox for business operations that
|
||||||
|
change data and enqueue work together. Use sagas only when a workflow crosses systems that cannot
|
||||||
|
share a transaction. Application code should retain product-specific policy; framework code owns
|
||||||
|
storage, validation, lifecycle, retries, diagnostics and scaffolding.
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
# `@wrnexus/payment` — proposal
|
||||||
|
|
||||||
|
**Status:** draft for review
|
||||||
|
**Date:** 2026-08-23
|
||||||
|
|
||||||
|
One package that gives an application real payments: gateway configuration, initialisation,
|
||||||
|
capture, refunds, stored methods, an admin surface, and UI components — so a team never writes
|
||||||
|
this again.
|
||||||
|
|
||||||
|
## The one rule that shapes everything
|
||||||
|
|
||||||
|
**The package never touches a raw card number.**
|
||||||
|
|
||||||
|
Every supported gateway offers hosted fields or a hosted checkout that tokenises the card in
|
||||||
|
the customer's browser, against the gateway's domain, before anything reaches your server. That
|
||||||
|
is what keeps an application in PCI **SAQ-A** — roughly a self-assessment questionnaire —
|
||||||
|
instead of SAQ-D, which is an audit programme with a six-figure floor.
|
||||||
|
|
||||||
|
So the package's UI components mount the gateway's own fields. `PayNow` renders a button that
|
||||||
|
opens a gateway session; it never renders an `<input name="cardNumber">`, and the package
|
||||||
|
exposes no API that would accept one. A framework that makes the cheap wrong thing easy will
|
||||||
|
have it done by someone in a hurry.
|
||||||
|
|
||||||
|
## The second rule: the webhook is the truth
|
||||||
|
|
||||||
|
A browser redirect saying "payment succeeded" is a claim from an untrusted client. The
|
||||||
|
authoritative event is the gateway's signed webhook.
|
||||||
|
|
||||||
|
So `checkPayment()` reads local state that webhooks maintain, and the local record is only ever
|
||||||
|
advanced by a verified webhook or a server-side gateway query — never by a client callback. The
|
||||||
|
redirect is a UX affordance: it tells the customer where to look, not the system what happened.
|
||||||
|
|
||||||
|
This is the single most common way a payment integration leaks money, and it is a design
|
||||||
|
decision, not a runtime check.
|
||||||
|
|
||||||
|
## Money model — the same discipline as metering
|
||||||
|
|
||||||
|
Payments are **append-only**, exactly like `@wrnexus/metering`'s ledger, and for the same
|
||||||
|
reason: a balance you cannot explain is a balance nobody trusts.
|
||||||
|
|
||||||
|
| Table | Holds |
|
||||||
|
| ---------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| `wrn_payment_intent` | one row per attempt: amount, currency, gateway, status, idempotency key |
|
||||||
|
| `wrn_payment_event` | append-only; every webhook and state transition, with the raw signed payload |
|
||||||
|
| `wrn_payment_refund` | one row per refund attempt, linked to its intent |
|
||||||
|
| `wrn_payment_method` | stored gateway tokens — never card data, never a PAN, at most a brand and last four |
|
||||||
|
| `wrn_payment_customer` | the mapping from your user id to each gateway's customer id |
|
||||||
|
|
||||||
|
A payment's current status is derived from its events, not from a mutable column that a race can
|
||||||
|
clobber. The `status` column on the intent is a cache of that derivation, and the package ships
|
||||||
|
a reconciliation command that recomputes it and reports disagreements.
|
||||||
|
|
||||||
|
**Idempotency is mandatory, not optional.** Every initialise and every refund carries a key; a
|
||||||
|
repeat with the same key returns the original result rather than charging twice. This is where a
|
||||||
|
double-click becomes a double-charge, and it should be impossible to opt out of.
|
||||||
|
|
||||||
|
## Gateways are adapters
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface PaymentGateway<TConfig = unknown> {
|
||||||
|
readonly id: string; // "stripe" | "razorpay" | "paypal" | ...
|
||||||
|
readonly capabilities: GatewayCapabilities;
|
||||||
|
readonly supports: { currencies: string[] | "any"; countries: string[] | "any" };
|
||||||
|
|
||||||
|
createIntent(input: CreateIntentInput): Promise<GatewayIntent>;
|
||||||
|
fetchIntent(intentRef: string): Promise<GatewayIntent>;
|
||||||
|
cancelIntent(intentRef: string, reason: string): Promise<GatewayIntent>;
|
||||||
|
refund(input: RefundInput): Promise<GatewayRefund>;
|
||||||
|
verifyWebhook(req: Request, secret: string): Promise<WebhookEvent>; // signature check
|
||||||
|
|
||||||
|
capture?(intentRef: string, amount?: Money): Promise<GatewayIntent>; // authorize-then-capture
|
||||||
|
listMethods?(customerRef: string): Promise<GatewayMethod[]>;
|
||||||
|
attachMethod?(customerRef: string, token: string): Promise<GatewayMethod>;
|
||||||
|
detachMethod?(methodRef: string): Promise<void>;
|
||||||
|
createCustomer?(input: CustomerInput): Promise<string>;
|
||||||
|
|
||||||
|
readonly clientConfig: (intent: GatewayIntent) => Record<string, string>; // safe to send
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`clientConfig` exists so the boundary is explicit: it returns exactly what the browser may see —
|
||||||
|
a publishable key and a session id, never a secret. Anything not returned by it cannot reach the
|
||||||
|
client, which is a structural guarantee rather than a code-review habit.
|
||||||
|
|
||||||
|
### Capabilities are declared, and the difference is refused loudly
|
||||||
|
|
||||||
|
This is the part that decides whether a multi-gateway abstraction holds. **Gateways are not
|
||||||
|
interchangeable.** Some have no authorise-then-capture. Some cannot do partial refunds. Some
|
||||||
|
have no stored-method vault. Some only settle in one currency.
|
||||||
|
|
||||||
|
An interface that pretends otherwise produces the worst failure mode there is: an application
|
||||||
|
calls `capturePayment()` against a gateway that has no such concept, and gets a confusing
|
||||||
|
gateway error at the moment money should have moved.
|
||||||
|
|
||||||
|
So every adapter declares what it can do, and the package refuses the rest **at call time with a
|
||||||
|
named reason** — and, better, at **build time** where the gateway is statically known:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface GatewayCapabilities {
|
||||||
|
authorizeThenCapture: boolean; // Stripe yes, Razorpay effectively no
|
||||||
|
partialRefund: boolean;
|
||||||
|
multipleRefunds: boolean;
|
||||||
|
storedMethods: boolean;
|
||||||
|
customerVault: boolean;
|
||||||
|
hostedFields: boolean; // inline hosted inputs
|
||||||
|
hostedCheckout: boolean; // full redirect/modal
|
||||||
|
webhookSignature: boolean; // an adapter without this is refused in production
|
||||||
|
payouts: boolean;
|
||||||
|
disputes: boolean;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`capabilitiesOf("razorpay").partialRefund` is a real answer an application can branch on, and
|
||||||
|
`RefundButton` reads it to decide whether to offer an amount field or only a full refund. A
|
||||||
|
capability an adapter does not declare is not merely unimplemented — it is refused, with a
|
||||||
|
message naming the gateway and the capability.
|
||||||
|
|
||||||
|
**`webhookSignature: false` is refused outright in production.** An adapter that cannot verify a
|
||||||
|
webhook cannot be trusted to tell you money moved, and the whole design rests on that.
|
||||||
|
|
||||||
|
### The roster
|
||||||
|
|
||||||
|
**Tier 1 — ship first, fully covered by the contract suite:**
|
||||||
|
|
||||||
|
| Adapter | Notes |
|
||||||
|
| ---------- | ---------------------------------------------------------------------------------- |
|
||||||
|
| `sandbox` | behaves like a real gateway, signed webhooks, injectable failure modes, no network |
|
||||||
|
| `stripe` | global; hosted fields, auth-then-capture, vault, partial and multiple refunds |
|
||||||
|
| `razorpay` | India-first; hosted checkout, INR-centric, no true auth-then-capture |
|
||||||
|
| `paypal` | global; hosted checkout, its own order/capture model |
|
||||||
|
|
||||||
|
Two real gateways prove the abstraction; one does not. Stripe and Razorpay are deliberately the
|
||||||
|
first pair because they **differ** — capture model, currency spread, refund semantics — so the
|
||||||
|
capability system is exercised rather than assumed.
|
||||||
|
|
||||||
|
**Tier 2 — same contract, added after the abstraction has survived Tier 1:**
|
||||||
|
`adyen`, `square`, `braintree`, `mollie`, `checkout.com`, `paddle` (merchant-of-record, so tax
|
||||||
|
and invoicing differ meaningfully).
|
||||||
|
|
||||||
|
**Tier 3 — regional, community-shaped:**
|
||||||
|
`payu`, `cashfree`, `phonepe`, `paytm` (India); `paystack`, `flutterwave` (Africa);
|
||||||
|
`midtrans`, `xendit` (South-East Asia); `authorize.net` (US legacy); `mercadopago` (LatAm).
|
||||||
|
|
||||||
|
### Any developer can add one
|
||||||
|
|
||||||
|
The roster is a starting set, not a ceiling. A custom adapter is a first-class citizen:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineGateway } from "@wrnexus/payment";
|
||||||
|
|
||||||
|
export const acme = defineGateway({
|
||||||
|
id: "acme",
|
||||||
|
capabilities: { partialRefund: true, storedMethods: false, webhookSignature: true /* … */ },
|
||||||
|
supports: { currencies: ["INR", "USD"], countries: ["IN"] },
|
||||||
|
async createIntent(input) {
|
||||||
|
/* … */
|
||||||
|
},
|
||||||
|
async verifyWebhook(req, secret) {
|
||||||
|
/* … */
|
||||||
|
},
|
||||||
|
// …
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`defineGateway` validates the shape at registration and runs the **shared contract suite** in
|
||||||
|
tests, so a third-party adapter is held to exactly the standard the built-in ones are. An adapter
|
||||||
|
that passes the suite behaves identically to Stripe's from the application's point of view; one
|
||||||
|
that does not, fails in CI rather than in production.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Config is **per-gateway and typed** — each adapter declares its own shape, so a missing
|
||||||
|
`webhookSecret` is a type error, not a 3am discovery:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// wrnexus.config.ts
|
||||||
|
payment: {
|
||||||
|
default: "stripe",
|
||||||
|
currency: "INR",
|
||||||
|
|
||||||
|
gateways: {
|
||||||
|
stripe: {
|
||||||
|
secretKey: env("STRIPE_SECRET_KEY"),
|
||||||
|
publishableKey: env("STRIPE_PUBLISHABLE_KEY"),
|
||||||
|
webhookSecret: env("STRIPE_WEBHOOK_SECRET"),
|
||||||
|
apiVersion: "2026-03-31",
|
||||||
|
captureMethod: "automatic", // or "manual" for auth-then-capture
|
||||||
|
},
|
||||||
|
razorpay: {
|
||||||
|
keyId: env("RAZORPAY_KEY_ID"),
|
||||||
|
keySecret: env("RAZORPAY_KEY_SECRET"),
|
||||||
|
webhookSecret: env("RAZORPAY_WEBHOOK_SECRET"),
|
||||||
|
theme: { color: "#0e7c86" },
|
||||||
|
},
|
||||||
|
acme: { apiKey: env("ACME_KEY"), webhookSecret: env("ACME_WEBHOOK_SECRET") },
|
||||||
|
},
|
||||||
|
|
||||||
|
// Optional: pick a gateway per payment instead of always using the default.
|
||||||
|
route(intent) {
|
||||||
|
if (intent.currency === "INR") return "razorpay";
|
||||||
|
if (intent.country === "US") return "stripe";
|
||||||
|
return "stripe";
|
||||||
|
},
|
||||||
|
|
||||||
|
sandbox: process.env.NODE_ENV !== "production",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Selection has three levels, most specific winning: an explicit `gateway` on the call, then
|
||||||
|
`route()`, then `default`. A `route()` that returns a gateway which is not configured, or which
|
||||||
|
cannot settle the intent's currency, is a startup error where it can be detected statically and
|
||||||
|
a named refusal where it cannot.
|
||||||
|
|
||||||
|
**Registering a custom adapter is a config line, not a fork:**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { acme } from "./payments/acme.ts";
|
||||||
|
payment: { adapters: [acme], default: "acme", /* … */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refuse to boot on a misconfiguration rather than failing at the first payment.** A live secret
|
||||||
|
key with `sandbox: true`, or a missing webhook secret, is a startup error naming the variable —
|
||||||
|
the same lesson as `APP_ENCRYPTION_KEY` failing as an opaque WebCrypto error until it was made
|
||||||
|
explicit.
|
||||||
|
|
||||||
|
## The helper surface
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Lifecycle
|
||||||
|
initializePayment(input): Promise<PaymentIntent> // amount, currency, subject, metadata, idempotencyKey
|
||||||
|
confirmPayment(id): Promise<PaymentIntent> // server-side confirm where the gateway needs it
|
||||||
|
capturePayment(id, amount?): Promise<PaymentIntent>// for auth-then-capture flows
|
||||||
|
cancelPayment(id, reason): Promise<PaymentIntent>
|
||||||
|
checkPayment(id): Promise<PaymentStatus> // derived from events, never from a client claim
|
||||||
|
syncPayment(id): Promise<PaymentIntent> // authoritative re-read from the gateway
|
||||||
|
|
||||||
|
// Refunds
|
||||||
|
refundPayment({ id, amount?, reason, idempotencyKey }): Promise<Refund> // partial by default
|
||||||
|
listRefunds(id): Promise<Refund[]>
|
||||||
|
refundableAmount(id): Promise<Money> // amount minus refunds already settled
|
||||||
|
|
||||||
|
// Stored methods
|
||||||
|
paymentMethods(userId): Promise<PaymentMethod[]>
|
||||||
|
attachPaymentMethod(userId, token): Promise<PaymentMethod>
|
||||||
|
detachPaymentMethod(methodId): Promise<void>
|
||||||
|
setDefaultPaymentMethod(userId, methodId): Promise<void>
|
||||||
|
|
||||||
|
// Records and reporting
|
||||||
|
getPayment(id) / listPayments(filter) // filter by user, status, gateway, date range
|
||||||
|
paymentTotals(filter): Promise<{ captured, refunded, net, byCurrency }>
|
||||||
|
reconcilePayments(range): Promise<Discrepancy[]> // local vs gateway, the operator's safety net
|
||||||
|
|
||||||
|
// Webhooks
|
||||||
|
paymentWebhookHandler(gatewayId): RouteHandler // signature-verified, idempotent, replay-safe
|
||||||
|
```
|
||||||
|
|
||||||
|
Every function that moves money is **non-throwing and returns a result** with a `fault`
|
||||||
|
discriminator, matching `@wrnexus/metering`: a refusal's reason is safe to show a customer, a
|
||||||
|
fault's reason belongs only in the log. That distinction was learned the hard way — a naive
|
||||||
|
catch once answered "payment required" with a raw SQLite message.
|
||||||
|
|
||||||
|
### `refundableAmount` earns its place
|
||||||
|
|
||||||
|
Partial refunds are where integrations quietly go wrong: two concurrent partial refunds each
|
||||||
|
check "is there enough left?", both see yes, and together exceed the capture. `refundPayment`
|
||||||
|
must enforce the cap with a conditional write — the same shape as metering's reserve — not with
|
||||||
|
a read-then-write.
|
||||||
|
|
||||||
|
## Migrations ship with the package
|
||||||
|
|
||||||
|
`@wrnexus/authz` provisioning nothing is a real cost in this codebase: every application
|
||||||
|
hand-wires `ensureAuthzTables`, and the framework's own example copies DDL by hand and silently
|
||||||
|
drifts. Payment must not repeat that.
|
||||||
|
|
||||||
|
The package is a **plugin**. It contributes its migrations, its webhook route, and its authz
|
||||||
|
permissions (`payment:read`, `payment:refund`, `payment:configure`) on install. An application
|
||||||
|
adds a config block and gets a working, guarded, migrated payment system.
|
||||||
|
|
||||||
|
## UI components
|
||||||
|
|
||||||
|
Every component mounts gateway-hosted fields; none collects card data itself.
|
||||||
|
|
||||||
|
| Component | Does |
|
||||||
|
| ------------------- | -------------------------------------------------------------------------------- |
|
||||||
|
| `PayNow` | the button: opens a gateway session, shows pending/success/failure, emits `paid` |
|
||||||
|
| `PaymentSheet` | hosted fields inline, with the gateway's own validation surfaced |
|
||||||
|
| `PaymentMethodList` | stored methods, set-default, detach — brand and last four only |
|
||||||
|
| `PaymentStatus` | live status for one intent, driven by `checkPayment` |
|
||||||
|
| `RefundButton` | admin-side, requires a reason, shows `refundableAmount` |
|
||||||
|
| `PaymentHistory` | a customer's payments and refunds |
|
||||||
|
| `PaymentSummary` | totals by status and currency for a range |
|
||||||
|
|
||||||
|
`PayNow` needs to survive the customer closing the tab mid-payment, so its resolved state comes
|
||||||
|
from `checkPayment`, not from whether the callback fired.
|
||||||
|
|
||||||
|
**One caveat to state plainly:** these components will hit `GAP-01` today. A `PaymentStatus`
|
||||||
|
that first appears client-side renders as an empty placeholder, and a `PayNow` whose props change
|
||||||
|
after mount will not update. Until the client component runtime lands, these components must be
|
||||||
|
built server-rendered-first with real navigation, exactly as the Sendline admin console was.
|
||||||
|
|
||||||
|
## Admin surface
|
||||||
|
|
||||||
|
A payments console — list and filter, view one payment with its full event timeline, issue a
|
||||||
|
refund with a required reason, inspect webhook deliveries and replay a failed one, and run
|
||||||
|
reconciliation. Every privileged action writes an audit entry, so the package should either take
|
||||||
|
a dependency on an audit interface or define one.
|
||||||
|
|
||||||
|
## Verification — the standard this project now holds
|
||||||
|
|
||||||
|
Unit and integration tests are necessary and insufficient. The package is done when:
|
||||||
|
|
||||||
|
1. Every gateway adapter — built-in **and** third-party — passes one shared contract suite, so
|
||||||
|
behaviour cannot drift per gateway. An adapter declaring a capability it does not honour fails
|
||||||
|
the suite; an adapter honouring one it does not declare fails too, because a silent extra is
|
||||||
|
how an application comes to depend on something the next gateway lacks.
|
||||||
|
2. The sandbox adapter can drive a full lifecycle offline: initialise, webhook, capture, partial
|
||||||
|
refund, over-refund refused, reconciliation clean.
|
||||||
|
3. **A real payment is driven end to end in a browser against a gateway's test mode**, and the
|
||||||
|
money is confirmed in the gateway's own dashboard — not in our database. Phase 3 proved that a
|
||||||
|
row saying `sent` is not the same as an email arriving; a row saying `captured` is not the same
|
||||||
|
as money moving.
|
||||||
|
4. A replayed webhook, a duplicated initialise, and a double-clicked refund each change the
|
||||||
|
ledger exactly once.
|
||||||
|
5. Reconciliation over a deliberately corrupted local row reports the discrepancy rather than
|
||||||
|
hiding it.
|
||||||
|
|
||||||
|
## What this package deliberately does not do
|
||||||
|
|
||||||
|
- **No card data, ever.** No PAN, no CVV, no expiry, in any API, table, log or component.
|
||||||
|
- **No invented gateway.** The sandbox adapter is clearly a sandbox and says so in its UI.
|
||||||
|
- **No subscription billing in v1.** Recurring is a genuinely separate problem — plans, proration,
|
||||||
|
dunning, retries — and bolting it on would compromise both.
|
||||||
|
- **No currency conversion.** Store and settle in the currency charged; a converted number in a
|
||||||
|
ledger is a number nobody can reconcile.
|
||||||
|
- **No silent capture of an expired authorisation.** It fails loudly.
|
||||||
|
|
||||||
|
## Build order
|
||||||
|
|
||||||
|
1. Core: schema, append-only events, status derivation, idempotency, non-throwing results
|
||||||
|
2. The capability system, `defineGateway`, and the shared adapter contract suite
|
||||||
|
3. The sandbox adapter — first consumer of the contract suite, and the thing every later
|
||||||
|
adapter is checked against
|
||||||
|
4. **Stripe, then Razorpay.** Deliberately this pair, because they differ on capture model,
|
||||||
|
currency spread and refund semantics. Building them together is what stops the abstraction
|
||||||
|
quietly becoming "Stripe, with names changed"
|
||||||
|
5. Gateway selection: explicit, `route()`, default — with the misconfiguration errors
|
||||||
|
6. The webhook route: signature verification, replay safety, event storage
|
||||||
|
7. Refunds, including the concurrent partial-refund cap and the `partialRefund: false` path
|
||||||
|
8. Stored methods and customers, behind `storedMethods`
|
||||||
|
9. PayPal — the third gateway, and the real test of whether Tier 2 can be added by someone who
|
||||||
|
did not design the abstraction
|
||||||
|
10. Admin console and reconciliation
|
||||||
|
11. UI components, capability-aware
|
||||||
|
12. Browser verification against a gateway test mode
|
||||||
+229
-5
@@ -91,6 +91,7 @@
|
|||||||
"AuthPluginOptions",
|
"AuthPluginOptions",
|
||||||
"AuthPublicUser",
|
"AuthPublicUser",
|
||||||
"AuthRandom",
|
"AuthRandom",
|
||||||
|
"AuthRequiredError",
|
||||||
"AuthResult",
|
"AuthResult",
|
||||||
"AuthRiskDecision",
|
"AuthRiskDecision",
|
||||||
"AuthRiskLevel",
|
"AuthRiskLevel",
|
||||||
@@ -210,6 +211,7 @@
|
|||||||
"signUpSchema",
|
"signUpSchema",
|
||||||
"totpUri",
|
"totpUri",
|
||||||
"tryGetDefaultAuthEngine",
|
"tryGetDefaultAuthEngine",
|
||||||
|
"validateProductionAuthConfig",
|
||||||
"verificationRequestSchema",
|
"verificationRequestSchema",
|
||||||
"verificationTokenSchema",
|
"verificationTokenSchema",
|
||||||
"verifyTotp"
|
"verifyTotp"
|
||||||
@@ -229,6 +231,7 @@
|
|||||||
],
|
],
|
||||||
"./middleware": [
|
"./middleware": [
|
||||||
"AUTH_SESSION_KEY",
|
"AUTH_SESSION_KEY",
|
||||||
|
"AuthRequiredError",
|
||||||
"AuthSessionOptions",
|
"AuthSessionOptions",
|
||||||
"RequireAuthOptions",
|
"RequireAuthOptions",
|
||||||
"authSession",
|
"authSession",
|
||||||
@@ -237,7 +240,8 @@
|
|||||||
"getAuthSession",
|
"getAuthSession",
|
||||||
"getAuthUser",
|
"getAuthUser",
|
||||||
"isAuthenticatedContext",
|
"isAuthenticatedContext",
|
||||||
"requireAuth"
|
"requireAuth",
|
||||||
|
"requireAuthUser"
|
||||||
],
|
],
|
||||||
"./passkeys": [
|
"./passkeys": [
|
||||||
"MemoryPasskeyChallengeStore",
|
"MemoryPasskeyChallengeStore",
|
||||||
@@ -252,11 +256,13 @@
|
|||||||
"./plugin": [
|
"./plugin": [
|
||||||
"AuthAuditIssue",
|
"AuthAuditIssue",
|
||||||
"AuthConfig",
|
"AuthConfig",
|
||||||
|
"AuthOAuthProviderConfig",
|
||||||
"AuthPluginOptions",
|
"AuthPluginOptions",
|
||||||
"AuthRoutesConfig",
|
"AuthRoutesConfig",
|
||||||
"authComponentsDir",
|
"authComponentsDir",
|
||||||
"authPlugin",
|
"authPlugin",
|
||||||
"default"
|
"default",
|
||||||
|
"validateProductionAuthConfig"
|
||||||
],
|
],
|
||||||
"./protector": [
|
"./protector": [
|
||||||
"createAuthSecretProtector"
|
"createAuthSecretProtector"
|
||||||
@@ -460,7 +466,9 @@
|
|||||||
"AUTHZ_LOCALS_KEY",
|
"AUTHZ_LOCALS_KEY",
|
||||||
"AttributeMeta",
|
"AttributeMeta",
|
||||||
"AuthorizationDecision",
|
"AuthorizationDecision",
|
||||||
|
"AuthorizationResponses",
|
||||||
"AuthorizeDecisionOptions",
|
"AuthorizeDecisionOptions",
|
||||||
|
"AuthorizedHandlerOptions",
|
||||||
"AuthzAuditEvent",
|
"AuthzAuditEvent",
|
||||||
"AuthzAuditSink",
|
"AuthzAuditSink",
|
||||||
"AuthzCatalog",
|
"AuthzCatalog",
|
||||||
@@ -476,10 +484,12 @@
|
|||||||
"GrantEffect",
|
"GrantEffect",
|
||||||
"GuardOptions",
|
"GuardOptions",
|
||||||
"MemoryAuditSink",
|
"MemoryAuditSink",
|
||||||
|
"OwnedResourceDefinition",
|
||||||
"PermissionMeta",
|
"PermissionMeta",
|
||||||
"PermissionStore",
|
"PermissionStore",
|
||||||
"Policy",
|
"Policy",
|
||||||
"Rbac",
|
"Rbac",
|
||||||
|
"RequestAuthorization",
|
||||||
"Subject",
|
"Subject",
|
||||||
"SubjectAssignments",
|
"SubjectAssignments",
|
||||||
"all",
|
"all",
|
||||||
@@ -497,7 +507,9 @@
|
|||||||
"createAuthzResolver",
|
"createAuthzResolver",
|
||||||
"decideFor",
|
"decideFor",
|
||||||
"decision",
|
"decision",
|
||||||
|
"defineAuthorizedHandler",
|
||||||
"defineAuthz",
|
"defineAuthz",
|
||||||
|
"defineOwnedResource",
|
||||||
"defineRbac",
|
"defineRbac",
|
||||||
"deniedBy",
|
"deniedBy",
|
||||||
"deny",
|
"deny",
|
||||||
@@ -507,6 +519,7 @@
|
|||||||
"filterCan",
|
"filterCan",
|
||||||
"generatePermissionTypes",
|
"generatePermissionTypes",
|
||||||
"getAuthzCatalog",
|
"getAuthzCatalog",
|
||||||
|
"getRequestAuthorization",
|
||||||
"guardPermission",
|
"guardPermission",
|
||||||
"hasAuthzCatalog",
|
"hasAuthzCatalog",
|
||||||
"hasRole",
|
"hasRole",
|
||||||
@@ -525,6 +538,23 @@
|
|||||||
"authzMigrationSql",
|
"authzMigrationSql",
|
||||||
"dbPermissionStore",
|
"dbPermissionStore",
|
||||||
"ensureAuthzTables"
|
"ensureAuthzTables"
|
||||||
|
],
|
||||||
|
"./defaults": [
|
||||||
|
"AuthzIdentityEvent",
|
||||||
|
"assignDefaultAuthzRoles",
|
||||||
|
"setDefaultAuthzRoleStore",
|
||||||
|
"setDefaultAuthzRoles"
|
||||||
|
],
|
||||||
|
"./plugin": [
|
||||||
|
"AuthzConfig",
|
||||||
|
"authzPlugin",
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"./runtime-memory-middleware": [
|
||||||
|
"default"
|
||||||
|
],
|
||||||
|
"./runtime-middleware": [
|
||||||
|
"default"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/benchmark": {
|
"@wrnexus/benchmark": {
|
||||||
@@ -938,6 +968,7 @@
|
|||||||
},
|
},
|
||||||
"@wrnexus/cli": {
|
"@wrnexus/cli": {
|
||||||
".": [],
|
".": [],
|
||||||
|
"./test-setup": [],
|
||||||
"./workspace": [
|
"./workspace": [
|
||||||
"ResolvedWorkspaceApp",
|
"ResolvedWorkspaceApp",
|
||||||
"ResolvedWorkspaceConfig",
|
"ResolvedWorkspaceConfig",
|
||||||
@@ -1137,6 +1168,8 @@
|
|||||||
"POSTGRES_TENANT_DIRECTORY_SCHEMA",
|
"POSTGRES_TENANT_DIRECTORY_SCHEMA",
|
||||||
"PageComponent",
|
"PageComponent",
|
||||||
"PageMeta",
|
"PageMeta",
|
||||||
|
"PaginationInput",
|
||||||
|
"PaginationOptions",
|
||||||
"PerformanceBudgets",
|
"PerformanceBudgets",
|
||||||
"PerformanceMeasurement",
|
"PerformanceMeasurement",
|
||||||
"PermissionsPolicyConfig",
|
"PermissionsPolicyConfig",
|
||||||
@@ -1198,6 +1231,7 @@
|
|||||||
"TenantResource",
|
"TenantResource",
|
||||||
"TenantSqlClient",
|
"TenantSqlClient",
|
||||||
"Tracer",
|
"Tracer",
|
||||||
|
"TransactionCookie",
|
||||||
"TrustedTypesConfig",
|
"TrustedTypesConfig",
|
||||||
"UploadError",
|
"UploadError",
|
||||||
"UploadInspectionResult",
|
"UploadInspectionResult",
|
||||||
@@ -1319,6 +1353,8 @@
|
|||||||
"ActionClientError",
|
"ActionClientError",
|
||||||
"ActionClientOptions",
|
"ActionClientOptions",
|
||||||
"ActionResult",
|
"ActionResult",
|
||||||
|
"ApiClientOptions",
|
||||||
|
"ApiRequest",
|
||||||
"ClientModuleScope",
|
"ClientModuleScope",
|
||||||
"HydrationScopeApi",
|
"HydrationScopeApi",
|
||||||
"NAV_RUNTIME",
|
"NAV_RUNTIME",
|
||||||
@@ -1332,6 +1368,7 @@
|
|||||||
"callServerFunction",
|
"callServerFunction",
|
||||||
"collectRefs",
|
"collectRefs",
|
||||||
"createActionClient",
|
"createActionClient",
|
||||||
|
"createApiClient",
|
||||||
"createOutputProxy",
|
"createOutputProxy",
|
||||||
"createServerProxy",
|
"createServerProxy",
|
||||||
"getActionRuntime",
|
"getActionRuntime",
|
||||||
@@ -1342,11 +1379,13 @@
|
|||||||
"invalidateClientModule",
|
"invalidateClientModule",
|
||||||
"invokeOutput",
|
"invokeOutput",
|
||||||
"loadClientFunctions",
|
"loadClientFunctions",
|
||||||
"registerOutputHandler"
|
"registerOutputHandler",
|
||||||
|
"useFetch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/db": {
|
"@wrnexus/db": {
|
||||||
".": [
|
".": [
|
||||||
|
"AddSeedOptions",
|
||||||
"BaseType",
|
"BaseType",
|
||||||
"Column",
|
"Column",
|
||||||
"ColumnDef",
|
"ColumnDef",
|
||||||
@@ -1357,6 +1396,7 @@
|
|||||||
"Dialect",
|
"Dialect",
|
||||||
"Driver",
|
"Driver",
|
||||||
"ExecResult",
|
"ExecResult",
|
||||||
|
"LedgerOptions",
|
||||||
"Migration",
|
"Migration",
|
||||||
"MigrationRunOptions",
|
"MigrationRunOptions",
|
||||||
"MigrationSafetyIssue",
|
"MigrationSafetyIssue",
|
||||||
@@ -1373,13 +1413,20 @@
|
|||||||
"RelationOptions",
|
"RelationOptions",
|
||||||
"Repository",
|
"Repository",
|
||||||
"Row",
|
"Row",
|
||||||
|
"SeedDatabase",
|
||||||
|
"SeedRow",
|
||||||
|
"SeedStep",
|
||||||
|
"SeedUserAccount",
|
||||||
"TxHandle",
|
"TxHandle",
|
||||||
|
"UpsertSeedOptions",
|
||||||
|
"addSeedData",
|
||||||
"analyzeMigrationSafety",
|
"analyzeMigrationSafety",
|
||||||
"analyzeMigrations",
|
"analyzeMigrations",
|
||||||
"appliedMigrations",
|
"appliedMigrations",
|
||||||
"applyMigrations",
|
"applyMigrations",
|
||||||
"batch",
|
"batch",
|
||||||
"closeDatabases",
|
"closeDatabases",
|
||||||
|
"compareAndSet",
|
||||||
"countRows",
|
"countRows",
|
||||||
"createDb",
|
"createDb",
|
||||||
"createRepository",
|
"createRepository",
|
||||||
@@ -1387,6 +1434,8 @@
|
|||||||
"cursorPaginate",
|
"cursorPaginate",
|
||||||
"databaseHealth",
|
"databaseHealth",
|
||||||
"databaseNames",
|
"databaseNames",
|
||||||
|
"defineLedger",
|
||||||
|
"defineSeed",
|
||||||
"exists",
|
"exists",
|
||||||
"firstOrThrow",
|
"firstOrThrow",
|
||||||
"generateQueriesFile",
|
"generateQueriesFile",
|
||||||
@@ -1404,15 +1453,20 @@
|
|||||||
"queryOperation",
|
"queryOperation",
|
||||||
"registerDb",
|
"registerDb",
|
||||||
"registerLazyDb",
|
"registerLazyDb",
|
||||||
|
"removeSeedData",
|
||||||
"resetDbPerformanceSnapshot",
|
"resetDbPerformanceSnapshot",
|
||||||
"retryTransaction",
|
"retryTransaction",
|
||||||
"rollback",
|
"rollback",
|
||||||
|
"runSeedQuery",
|
||||||
"scaffoldMigration",
|
"scaffoldMigration",
|
||||||
|
"seedIfMissing",
|
||||||
|
"seedUsers",
|
||||||
"setDb",
|
"setDb",
|
||||||
"softDeleteClause",
|
"softDeleteClause",
|
||||||
"status",
|
"status",
|
||||||
"table",
|
"table",
|
||||||
"tenantScope",
|
"tenantScope",
|
||||||
|
"upsertSeedData",
|
||||||
"v",
|
"v",
|
||||||
"withTransaction"
|
"withTransaction"
|
||||||
],
|
],
|
||||||
@@ -1613,6 +1667,7 @@
|
|||||||
"EncryptionKey",
|
"EncryptionKey",
|
||||||
"EncryptionKeyring",
|
"EncryptionKeyring",
|
||||||
"ReplayStore",
|
"ReplayStore",
|
||||||
|
"SecretResourceOptions",
|
||||||
"createEncryptedRequest",
|
"createEncryptedRequest",
|
||||||
"createKeyring",
|
"createKeyring",
|
||||||
"createMemoryReplayStore",
|
"createMemoryReplayStore",
|
||||||
@@ -1620,6 +1675,7 @@
|
|||||||
"decryptEncryptedResponse",
|
"decryptEncryptedResponse",
|
||||||
"decryptHttpBody",
|
"decryptHttpBody",
|
||||||
"decryptRequest",
|
"decryptRequest",
|
||||||
|
"defineSecretResource",
|
||||||
"deriveKey",
|
"deriveKey",
|
||||||
"encrypt",
|
"encrypt",
|
||||||
"encryptHttpBody",
|
"encryptHttpBody",
|
||||||
@@ -1653,26 +1709,39 @@
|
|||||||
"@wrnexus/helpers": {
|
"@wrnexus/helpers": {
|
||||||
".": [
|
".": [
|
||||||
"AllowedHosts",
|
"AllowedHosts",
|
||||||
|
"ApiHandler",
|
||||||
|
"HttpError",
|
||||||
"LoginRedirectOptions",
|
"LoginRedirectOptions",
|
||||||
"OriginalRequestOptions",
|
"OriginalRequestOptions",
|
||||||
"RequestContext",
|
"RequestContext",
|
||||||
|
"ResourceDefinition",
|
||||||
|
"ResourceHandlers",
|
||||||
"RetryOptions",
|
"RetryOptions",
|
||||||
"appOrigin",
|
"appOrigin",
|
||||||
"appUrl",
|
"appUrl",
|
||||||
|
"authorized",
|
||||||
"backoffDelay",
|
"backoffDelay",
|
||||||
"clamp",
|
"clamp",
|
||||||
"currentAppName",
|
"currentAppName",
|
||||||
"currentAppOrigin",
|
"currentAppOrigin",
|
||||||
|
"defineApiRoute",
|
||||||
|
"defineResource",
|
||||||
"getOriginalRequestMethod",
|
"getOriginalRequestMethod",
|
||||||
"getOriginalRequestOrigin",
|
"getOriginalRequestOrigin",
|
||||||
"getOriginalRequestPath",
|
"getOriginalRequestPath",
|
||||||
"getOriginalRequestUrl",
|
"getOriginalRequestUrl",
|
||||||
|
"json",
|
||||||
"once",
|
"once",
|
||||||
"redirectToLogin",
|
"redirectToLogin",
|
||||||
|
"requireJson",
|
||||||
|
"requireParam",
|
||||||
|
"requirePermission",
|
||||||
|
"requireUser",
|
||||||
"retry",
|
"retry",
|
||||||
"safeJsonParse",
|
"safeJsonParse",
|
||||||
"sleep",
|
"sleep",
|
||||||
"stableStringify",
|
"stableStringify",
|
||||||
|
"subjectId",
|
||||||
"withTimeout",
|
"withTimeout",
|
||||||
"workspaceAppOrigins",
|
"workspaceAppOrigins",
|
||||||
"workspaceRootDomain"
|
"workspaceRootDomain"
|
||||||
@@ -1864,7 +1933,27 @@
|
|||||||
"./server": [
|
"./server": [
|
||||||
"ApiCallCompletionItem",
|
"ApiCallCompletionItem",
|
||||||
"apiCallCompletions",
|
"apiCallCompletions",
|
||||||
"apiCallHover"
|
"apiCallHover",
|
||||||
|
"isApiReferenceAt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@wrnexus/mail": {
|
||||||
|
".": [
|
||||||
|
"MailDriver",
|
||||||
|
"MailMessage",
|
||||||
|
"MailOptions",
|
||||||
|
"MailSandboxDecision",
|
||||||
|
"MailTemplate",
|
||||||
|
"SealedMailCredentialsOptions",
|
||||||
|
"SealedMailDriverOptions",
|
||||||
|
"defineMail",
|
||||||
|
"defineMailTemplate",
|
||||||
|
"defineSealedMailCredentials",
|
||||||
|
"interpolateTemplate",
|
||||||
|
"mailSandboxDecision",
|
||||||
|
"queuedMail",
|
||||||
|
"sealedMailDriver",
|
||||||
|
"storedMailTemplate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/mcp": {
|
"@wrnexus/mcp": {
|
||||||
@@ -1878,6 +1967,20 @@
|
|||||||
"runMcpStdio"
|
"runMcpStdio"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@wrnexus/metering": {
|
||||||
|
".": [
|
||||||
|
"EntitlementPlan",
|
||||||
|
"EntitlementsOptions",
|
||||||
|
"MeterKind",
|
||||||
|
"MeterOptions",
|
||||||
|
"MeterPack",
|
||||||
|
"MeterResult",
|
||||||
|
"MeterStore",
|
||||||
|
"defineEntitlements",
|
||||||
|
"defineMeter",
|
||||||
|
"definePacks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@wrnexus/mobile": {
|
"@wrnexus/mobile": {
|
||||||
".": [
|
".": [
|
||||||
"CapacitorBridge",
|
"CapacitorBridge",
|
||||||
@@ -2078,6 +2181,70 @@
|
|||||||
"traceMiddleware"
|
"traceMiddleware"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@wrnexus/payment": {
|
||||||
|
".": [
|
||||||
|
"CreateIntentInput",
|
||||||
|
"CustomerInput",
|
||||||
|
"GatewayCapabilities",
|
||||||
|
"GatewayContractFixture",
|
||||||
|
"GatewayContractReport",
|
||||||
|
"GatewayIntent",
|
||||||
|
"GatewayMethod",
|
||||||
|
"GatewayRefund",
|
||||||
|
"GatewaySupport",
|
||||||
|
"InitializePaymentInput",
|
||||||
|
"Money",
|
||||||
|
"PayPalConfig",
|
||||||
|
"PaymentEvent",
|
||||||
|
"PaymentGateway",
|
||||||
|
"PaymentIntent",
|
||||||
|
"PaymentMethod",
|
||||||
|
"PaymentOptions",
|
||||||
|
"PaymentRefund",
|
||||||
|
"PaymentResult",
|
||||||
|
"PaymentService",
|
||||||
|
"PaymentStatus",
|
||||||
|
"PaymentStore",
|
||||||
|
"RazorpayConfig",
|
||||||
|
"RefundInput",
|
||||||
|
"SandboxOptions",
|
||||||
|
"StripeConfig",
|
||||||
|
"WebhookEvent",
|
||||||
|
"configurePayment",
|
||||||
|
"databasePaymentStore",
|
||||||
|
"defineGateway",
|
||||||
|
"definePayment",
|
||||||
|
"derivePaymentStatus",
|
||||||
|
"exerciseGatewayContract",
|
||||||
|
"getPaymentService",
|
||||||
|
"memoryPaymentStore",
|
||||||
|
"paymentPlugin",
|
||||||
|
"paypalGateway",
|
||||||
|
"razorpayGateway",
|
||||||
|
"sandboxGateway",
|
||||||
|
"signSandboxWebhook",
|
||||||
|
"stripeGateway",
|
||||||
|
"verifyGatewayContract"
|
||||||
|
],
|
||||||
|
"./adapters": [
|
||||||
|
"PayPalConfig",
|
||||||
|
"RazorpayConfig",
|
||||||
|
"SandboxOptions",
|
||||||
|
"StripeConfig",
|
||||||
|
"paypalGateway",
|
||||||
|
"razorpayGateway",
|
||||||
|
"sandboxGateway",
|
||||||
|
"signSandboxWebhook",
|
||||||
|
"stripeGateway"
|
||||||
|
],
|
||||||
|
"./plugin": [
|
||||||
|
"default",
|
||||||
|
"paymentPlugin"
|
||||||
|
],
|
||||||
|
"./runtime-webhook": [
|
||||||
|
"POST"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@wrnexus/playground": {
|
"@wrnexus/playground": {
|
||||||
".": [
|
".": [
|
||||||
"PlaygroundCompilation",
|
"PlaygroundCompilation",
|
||||||
@@ -2242,23 +2409,44 @@
|
|||||||
"@wrnexus/queue": {
|
"@wrnexus/queue": {
|
||||||
".": [
|
".": [
|
||||||
"AddOptions",
|
"AddOptions",
|
||||||
|
"DefineQueueOptions",
|
||||||
|
"DefinedJob",
|
||||||
|
"DefinedJobOptions",
|
||||||
|
"DefinedJobs",
|
||||||
|
"DefinedQueue",
|
||||||
"DurableQueue",
|
"DurableQueue",
|
||||||
"DurableQueueOptions",
|
"DurableQueueOptions",
|
||||||
"Job",
|
"Job",
|
||||||
"JobContext",
|
"JobContext",
|
||||||
"JobDefinition",
|
"JobDefinition",
|
||||||
"JobHandler",
|
"JobHandler",
|
||||||
|
"JobStateMachineOptions",
|
||||||
|
"JobStatus",
|
||||||
|
"OutboxEntry",
|
||||||
|
"OutboxWriter",
|
||||||
"POSTGRES_QUEUE_SCHEMA",
|
"POSTGRES_QUEUE_SCHEMA",
|
||||||
"Queue",
|
"Queue",
|
||||||
|
"QueueAdminOptions",
|
||||||
"QueueDashboardSnapshot",
|
"QueueDashboardSnapshot",
|
||||||
|
"QueueEvent",
|
||||||
|
"QueueEventType",
|
||||||
|
"QueueHealth",
|
||||||
|
"QueueJobDefinitions",
|
||||||
|
"QueueJobRecord",
|
||||||
|
"QueueJobState",
|
||||||
"QueueOptions",
|
"QueueOptions",
|
||||||
"QueueScheduler",
|
"QueueScheduler",
|
||||||
|
"QueueStorage",
|
||||||
|
"QueueStorageConfig",
|
||||||
"QueueStore",
|
"QueueStore",
|
||||||
"RedisQueueClient",
|
"RedisQueueClient",
|
||||||
|
"SagaStep",
|
||||||
"ScheduledJob",
|
"ScheduledJob",
|
||||||
"SqlQueueClient",
|
"SqlQueueClient",
|
||||||
|
"SqliteQueueClient",
|
||||||
"SubjectJob",
|
"SubjectJob",
|
||||||
"SubjectQueue",
|
"SubjectQueue",
|
||||||
|
"WorkerDefinition",
|
||||||
"WorkflowDefinition",
|
"WorkflowDefinition",
|
||||||
"WorkflowEngine",
|
"WorkflowEngine",
|
||||||
"WorkflowRunContext",
|
"WorkflowRunContext",
|
||||||
@@ -2267,22 +2455,46 @@
|
|||||||
"WorkflowStep",
|
"WorkflowStep",
|
||||||
"WorkflowStore",
|
"WorkflowStore",
|
||||||
"addBatch",
|
"addBatch",
|
||||||
|
"configureQueueStorage",
|
||||||
|
"configuredQueueStore",
|
||||||
"createDurableQueue",
|
"createDurableQueue",
|
||||||
"createQueue",
|
"createQueue",
|
||||||
|
"createQueueAdminHandler",
|
||||||
"createQueueScheduler",
|
"createQueueScheduler",
|
||||||
"createWorkflowEngine",
|
"createWorkflowEngine",
|
||||||
"cronToInterval",
|
"cronToInterval",
|
||||||
|
"databaseQueueStore",
|
||||||
"defineDurableWorkflow",
|
"defineDurableWorkflow",
|
||||||
"defineJob",
|
"defineJob",
|
||||||
|
"defineJobStateMachine",
|
||||||
|
"defineQueue",
|
||||||
|
"defineSaga",
|
||||||
|
"defineWorker",
|
||||||
"defineWorkflow",
|
"defineWorkflow",
|
||||||
|
"drainOutbox",
|
||||||
|
"installDatabaseQueueSchema",
|
||||||
|
"installOutboxSchema",
|
||||||
|
"installSqliteQueueSchema",
|
||||||
"memoryQueueStore",
|
"memoryQueueStore",
|
||||||
"memoryWorkflowStore",
|
"memoryWorkflowStore",
|
||||||
"postgresQueueStore",
|
"postgresQueueStore",
|
||||||
"queueDashboardSnapshot",
|
"queueDashboardSnapshot",
|
||||||
|
"queueStorageConfig",
|
||||||
"redisQueueStore",
|
"redisQueueStore",
|
||||||
"renderQueueDashboard",
|
"renderQueueDashboard",
|
||||||
"runQueueDaemon",
|
"runQueueDaemon",
|
||||||
"subjectQueue"
|
"runWorker",
|
||||||
|
"sqliteQueueSchema",
|
||||||
|
"sqliteQueueStore",
|
||||||
|
"subjectQueue",
|
||||||
|
"testQueue",
|
||||||
|
"withOutbox"
|
||||||
|
],
|
||||||
|
"./sqlite": [
|
||||||
|
"SqliteQueueClient",
|
||||||
|
"installSqliteQueueSchema",
|
||||||
|
"sqliteQueueSchema",
|
||||||
|
"sqliteQueueStore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/react": {
|
"@wrnexus/react": {
|
||||||
@@ -2325,6 +2537,7 @@
|
|||||||
".": [
|
".": [
|
||||||
"AnimationTimeline",
|
"AnimationTimeline",
|
||||||
"Cleanup",
|
"Cleanup",
|
||||||
|
"FormState",
|
||||||
"HistorySignal",
|
"HistorySignal",
|
||||||
"ReactiveContext",
|
"ReactiveContext",
|
||||||
"ReactiveScope",
|
"ReactiveScope",
|
||||||
@@ -2351,6 +2564,7 @@
|
|||||||
"transition",
|
"transition",
|
||||||
"untrack",
|
"untrack",
|
||||||
"urlSignal",
|
"urlSignal",
|
||||||
|
"useForm",
|
||||||
"watch"
|
"watch"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2638,6 +2852,7 @@
|
|||||||
".": [
|
".": [
|
||||||
"ACCENT_COOKIE",
|
"ACCENT_COOKIE",
|
||||||
"AppConfig",
|
"AppConfig",
|
||||||
|
"AppConfigInput",
|
||||||
"BrowserCookieApi",
|
"BrowserCookieApi",
|
||||||
"BrowserCookieOptions",
|
"BrowserCookieOptions",
|
||||||
"BrowserCookiePreference",
|
"BrowserCookiePreference",
|
||||||
@@ -2662,6 +2877,7 @@
|
|||||||
"ObservabilityConfig",
|
"ObservabilityConfig",
|
||||||
"PerformanceConfig",
|
"PerformanceConfig",
|
||||||
"PwaConfig",
|
"PwaConfig",
|
||||||
|
"ResolvedAppConfig",
|
||||||
"ResolvedConfigLayers",
|
"ResolvedConfigLayers",
|
||||||
"ResolvedTheme",
|
"ResolvedTheme",
|
||||||
"StyleProcessContext",
|
"StyleProcessContext",
|
||||||
@@ -2876,10 +3092,14 @@
|
|||||||
".": [
|
".": [
|
||||||
"BrowserArtifactPage",
|
"BrowserArtifactPage",
|
||||||
"Deferred",
|
"Deferred",
|
||||||
|
"DetectMutationsOptions",
|
||||||
|
"FetchMock",
|
||||||
"Harness",
|
"Harness",
|
||||||
"HarnessOptions",
|
"HarnessOptions",
|
||||||
"JsonResponse",
|
"JsonResponse",
|
||||||
"MemoryCookieJar",
|
"MemoryCookieJar",
|
||||||
|
"MutationCase",
|
||||||
|
"MutationReport",
|
||||||
"TestRequestOptions",
|
"TestRequestOptions",
|
||||||
"TransactionalDatabase",
|
"TransactionalDatabase",
|
||||||
"WaitForOptions",
|
"WaitForOptions",
|
||||||
@@ -2891,9 +3111,13 @@
|
|||||||
"captureBrowserArtifacts",
|
"captureBrowserArtifacts",
|
||||||
"createContext",
|
"createContext",
|
||||||
"createFactory",
|
"createFactory",
|
||||||
|
"createFetchMock",
|
||||||
"createHarness",
|
"createHarness",
|
||||||
|
"createTestApp",
|
||||||
|
"createTestContext",
|
||||||
"deferred",
|
"deferred",
|
||||||
"describe",
|
"describe",
|
||||||
|
"detectMutations",
|
||||||
"expect",
|
"expect",
|
||||||
"expectProblem",
|
"expectProblem",
|
||||||
"it",
|
"it",
|
||||||
|
|||||||
@@ -4,22 +4,22 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
|
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
|
||||||
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b",
|
"packages/ui/components/AdvancedSelect.wrn": "b952b894a5b35050fc2f6e43c4182101eeee05c75662e2aca4abfe61512b305b",
|
||||||
"packages/ui/components/AnnouncementBar.wrn": "cb66b8d0619ee798a115b43f0ad93fda93dfb46571589e7a9f0a8d65ec3c84c9",
|
"packages/ui/components/AnnouncementBar.wrn": "5b3219db555e0fcdb94ec41dc18186ee1b7c0cf9c260469f7fb1fc43435c28c4",
|
||||||
"packages/ui/components/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
|
"packages/ui/components/AuthForm.wrn": "46a3e1db1c9e69efc52473cbf2fababdcbfe7144e7571469638d2c3addd4d7f0",
|
||||||
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
|
"packages/ui/components/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
|
||||||
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97",
|
"packages/ui/components/AvatarGroup.wrn": "30b702e06a1adff8b9bae32820206020e086a69560cbdcbadb19f04f25951f97",
|
||||||
"packages/ui/components/BackToTop.wrn": "5b760ec7941ffd8e58c5dedae1b322ee46e072e927533d7ec077b511a816e436",
|
"packages/ui/components/BackToTop.wrn": "d4b8d1fd37ff26c89b4276ce54ce6ac9581dc2ff8bbcbe160e072778bba76eab",
|
||||||
"packages/ui/components/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
|
"packages/ui/components/Blockquote.wrn": "3761bf13babb6eb07ce7392258ee439ef5478064cf589eff63f945b950e00d2e",
|
||||||
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
|
"packages/ui/components/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
|
||||||
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
|
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
|
||||||
"packages/ui/components/CTASection.wrn": "d0665f2aa33eea95e6f84120f739b3dd9c8723c882138bcb1a7f80dcd7909904",
|
"packages/ui/components/CTASection.wrn": "f6872b980432d5ee9d88388250be9838ccce51bd706349b283ef472ee692b237",
|
||||||
"packages/ui/components/Card.wrn": "3cc2ff238bda279c169026a462236144ad11ec0046deef00ab00e001a07e8a4c",
|
"packages/ui/components/Card.wrn": "be8cd6a438d1ede4c284a302b76f0fbc235a2423b83a7ced21131763fecc8a90",
|
||||||
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
|
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
|
||||||
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
|
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
|
||||||
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
|
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
|
||||||
"packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30",
|
"packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30",
|
||||||
"packages/ui/components/Collapse.wrn": "26df087a61c82f1aa1749785f4c9be654c4f934859c1a47acfcb8cc67eecc75d",
|
"packages/ui/components/Collapse.wrn": "26df087a61c82f1aa1749785f4c9be654c4f934859c1a47acfcb8cc67eecc75d",
|
||||||
"packages/ui/components/ColorPicker.wrn": "557e36adbdee66d8ba9dcdc2446c13f3385a243f2766777feef8504908006c99",
|
"packages/ui/components/ColorPicker.wrn": "ccac7eb49c0f443b404004f1298fa905fde45c52b51466194920939c920348c9",
|
||||||
"packages/ui/components/Columns.wrn": "8ccd9e53f6799b09f8b996740d12e4e73ee782f6fd3e57595c6c16f788b9b812",
|
"packages/ui/components/Columns.wrn": "8ccd9e53f6799b09f8b996740d12e4e73ee782f6fd3e57595c6c16f788b9b812",
|
||||||
"packages/ui/components/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1",
|
"packages/ui/components/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1",
|
||||||
"packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e",
|
"packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437",
|
"packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437",
|
||||||
"packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3",
|
"packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3",
|
||||||
"packages/ui/components/DataTable.wrn": "e929fdb4e59613878a03fb6a11e09c42f773c06ca7b20e43ae16a266835f2af4",
|
"packages/ui/components/DataTable.wrn": "e929fdb4e59613878a03fb6a11e09c42f773c06ca7b20e43ae16a266835f2af4",
|
||||||
"packages/ui/components/DatePicker.wrn": "91bcfd3832d3bccc4de5402c0c06209947bde593bad4f869b4c29ee14a5c0bef",
|
"packages/ui/components/DatePicker.wrn": "cefbbb2abb57ad4c57864a0c65589592a20f9ea1695f9fc7693c3135dd82e71a",
|
||||||
"packages/ui/components/DeviceFrame.wrn": "c38d25419ee44b05d7c443cc4e854aeeb7a53a9289e374d71834de2dd2e4d979",
|
"packages/ui/components/DeviceFrame.wrn": "c38d25419ee44b05d7c443cc4e854aeeb7a53a9289e374d71834de2dd2e4d979",
|
||||||
"packages/ui/components/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019",
|
"packages/ui/components/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019",
|
||||||
"packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513",
|
"packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513",
|
||||||
@@ -36,15 +36,15 @@
|
|||||||
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
|
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
|
||||||
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
|
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
|
||||||
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
|
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
|
||||||
"packages/ui/components/FeatureIconCard.wrn": "3b4f3fa62c6729e686886a5b848e26c255766684829cec2b715967b4e73d6f07",
|
"packages/ui/components/FeatureIconCard.wrn": "5f65d64235babc3e653a5695204412202015e6f3ffadca566fc18c7f9ce70428",
|
||||||
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8",
|
"packages/ui/components/FileInput.wrn": "611047c362458a4e2513b741cc3ef43fbe54c10ddafc13775e4ff39c9de34931",
|
||||||
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034",
|
"packages/ui/components/FileUploadProgress.wrn": "ba00557afebf60e6221385d6efcf08635789328deee6e551fc53fbb71e1f413b",
|
||||||
"packages/ui/components/Footer.wrn": "f7c5a77064a09e244f689d42475c4c20143f4c8859cac451c10a50c14652db33",
|
"packages/ui/components/Footer.wrn": "e3515e05ef3ef51e6dad71d67ddfe110c80fdcf69e952aecdc7973e69d4f08c7",
|
||||||
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
|
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
|
||||||
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
|
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
|
||||||
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
|
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
|
||||||
"packages/ui/components/Image.wrn": "361179c478d7674c3b998891a366d73de9514fc6cc8786440c6c50b9ef065357",
|
"packages/ui/components/Image.wrn": "361179c478d7674c3b998891a366d73de9514fc6cc8786440c6c50b9ef065357",
|
||||||
"packages/ui/components/InputGroup.wrn": "30e9f2e4364542179180b6d6e796002d0ffe1e30305146749874697cde6629ea",
|
"packages/ui/components/InputGroup.wrn": "9e259d15207500d07ff626f09c2e2f5eaf4e471f805a7bdbec154b1a23ce0f8d",
|
||||||
"packages/ui/components/InputNumber.wrn": "069195cc483f3f7d6f305d03fbe5a821906a030376fafb6f0d969e4c95b68fd0",
|
"packages/ui/components/InputNumber.wrn": "069195cc483f3f7d6f305d03fbe5a821906a030376fafb6f0d969e4c95b68fd0",
|
||||||
"packages/ui/components/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9",
|
"packages/ui/components/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9",
|
||||||
"packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1",
|
"packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1",
|
||||||
@@ -55,54 +55,54 @@
|
|||||||
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
|
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
|
||||||
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
|
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
|
||||||
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
|
"packages/ui/components/Marquee.wrn": "81cd80732be34c8d7e4bf28b00347e1a9273166afdf66589d9f5d5774b69403b",
|
||||||
"packages/ui/components/MegaMenu.wrn": "6a6db166259f8f2bd7ac8fbb21dba6d915c4b936d87b19805caa78189f54730a",
|
"packages/ui/components/MegaMenu.wrn": "a34a84f31f7a3016cf5a607a22f8f7b2322552c550278f6e8c5e6d6cb18848a8",
|
||||||
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
|
"packages/ui/components/MetricCard.wrn": "513a6e99be3d9db46d96af9942b51331a0a048b184603e77e814506f0a52bdc5",
|
||||||
"packages/ui/components/MetricGrid.wrn": "c71e53249835908833055b553e64a8ee55fdf11ac4605436a9581ebb87a0608b",
|
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
|
||||||
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
||||||
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
||||||
"packages/ui/components/Navbar.wrn": "01903230210cd2e5e6d9325a0708f85af9623d4118e3a53f1adb770fd545425d",
|
"packages/ui/components/Navbar.wrn": "b68a824e479ce6957afc3e9710442281cdc1a8d02cd96501fe060271f54368e4",
|
||||||
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
|
"packages/ui/components/PageHeader.wrn": "29a93cdca7285548b79ebe5b6421eef6dbb4d5ae8be2508fa2d681e1068ba07f",
|
||||||
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
||||||
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
||||||
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
|
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
|
||||||
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee",
|
"packages/ui/components/PortalDashboard.wrn": "f04171eb1f8cabef06de3c20e4694b766def3407e1e20d2c9aaebc9bb7da03ee",
|
||||||
"packages/ui/components/PreferenceSwitcher.wrn": "77229e15449182563ebf49159507139928eb74e799e4438e00e9aa81fd60f982",
|
"packages/ui/components/PreferenceSwitcher.wrn": "519cf911390f529fff3fd16e11fb58e79d618fe851902202dc37552ee1182395",
|
||||||
"packages/ui/components/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
|
"packages/ui/components/PublicPageShell.wrn": "3d3f25c03d48f3d3d7e3c567e79cb137c7fce1867e054fd6d440fa1d2e961077",
|
||||||
"packages/ui/components/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b",
|
"packages/ui/components/RangeSlider.wrn": "e8ab41782c9d6e58638f0bfccf6e3172af4ebf552189d2199cc6c96bd1652067",
|
||||||
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587",
|
"packages/ui/components/Rating.wrn": "902b55c64e3798f3097f09b1558d1a2372f66d4f760662de085783108ee04587",
|
||||||
"packages/ui/components/Scrollspy.wrn": "b9c6400ee92a7c228362610a2051f37bff1d3baba1dbacd66d58397dfda5d856",
|
"packages/ui/components/Scrollspy.wrn": "4b452fcbd524feda9c9f2c81894ee01747a51888e3b86fafb37c5d4d192a3a51",
|
||||||
"packages/ui/components/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
|
"packages/ui/components/SearchBox.wrn": "3f88340e0b69c0cd552023c72bd22dc511bc00422d46060097bcf0777e683675",
|
||||||
"packages/ui/components/Section.wrn": "45b7a1977c25a6020d5cc8bab2ce9c80730e7ec2d51ad53e8c9f64cd9b5b64cd",
|
"packages/ui/components/Section.wrn": "78da13eb8bc1acafe8e6151cbaeed4baaac2ab860e3209f1d13c467f279d2e68",
|
||||||
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
|
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
|
||||||
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
|
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
|
||||||
"packages/ui/components/SplitHero.wrn": "843387dc7b920b3cf6a1b2b785b8653c281982e524479edc2638a89381ff3577",
|
"packages/ui/components/SplitHero.wrn": "7127cb869d1228522d066e028818dd9e512de8b6dd8f99e3dec500e6da5a3cb0",
|
||||||
"packages/ui/components/StatsBar.wrn": "c273bd6ca417a0a20e5e6f1214414f678741b73bedd885fea1037575fa8c5b95",
|
"packages/ui/components/StatsBar.wrn": "6fabcb4374b1e074f58ebcaba968f235c8a3409762163aea51298e2515a14435",
|
||||||
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
|
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
|
||||||
"packages/ui/components/StrongPassword.wrn": "d5ea906fafe268b070a26ab551a252c0575d899c9d2b03b337bd047688b85dc5",
|
"packages/ui/components/StrongPassword.wrn": "cc31cb080c4b4116c544b4340b8d740d08fec1b05563372b3280c50759416c87",
|
||||||
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
|
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
|
||||||
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
|
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
|
||||||
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954",
|
"packages/ui/components/TextLink.wrn": "9d561631c5f65a2bcdc59f5c0b166a5dd794a396f42093f2cd26388386f6f954",
|
||||||
"packages/ui/components/TimePicker.wrn": "ec0860a4a82a042a65acf9aa81519a5c815b61e0b8080cf6f1c986d26ce7f0e8",
|
"packages/ui/components/TimePicker.wrn": "33bd7a7069b7abe858cf1617b5b950733b37a8d522469ed673cff00b4dfcad0e",
|
||||||
"packages/ui/components/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
|
"packages/ui/components/Timeline.wrn": "fb553f69763840532eef1c0aafa79013f57411dc5335e7cb7e7c20c5a3753008",
|
||||||
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
|
"packages/ui/components/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
|
||||||
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
|
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
|
||||||
"packages/ui/components/TogglePassword.wrn": "b6176453cb2c586fe88c3db6bdd734ae2b4c95db46d57e0de298dfba3d0dd0c2",
|
"packages/ui/components/TogglePassword.wrn": "caa58479e5c8b1e3e504181a56a7d6bcf71d0a230010733710961c31e1cc58ab",
|
||||||
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
|
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
|
||||||
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
|
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
|
||||||
"packages/ui/components/WysiwygEditor.wrn": "3f9d1765184581e3c1f335769a9f19969917450258bc95ba076980d99f5610b5",
|
"packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
|
||||||
"packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d",
|
"packages/ui/components/alert.wrn": "d42287e41b918b1e19962e9462ef203002eb5480acac984f115b711e9dbe663d",
|
||||||
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
|
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
|
||||||
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
|
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
|
||||||
"packages/ui/components/button.wrn": "cba4ccbbd23bb75b3836ec7a53673e40f1e043d18bfcee3d613db96c318f4ba8",
|
"packages/ui/components/button.wrn": "65570c58cb2547f1e17cd158eec7cf7b1ffc721e6976c6486c9df564627b1219",
|
||||||
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4",
|
"packages/ui/components/checkbox.wrn": "da735354b57f33946591e1bd018b521ff709bae1325e2317735824673188377a",
|
||||||
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847",
|
"packages/ui/components/input.wrn": "2f17353cbf0711ca50a935ae03ee9ee53594d7e35eb73f5df573fadb7958fac2",
|
||||||
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
|
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
|
||||||
"packages/ui/components/radio.wrn": "09425a78358de5bbd2f47482f313e80065135335b969ce5ccdd5c7cc3ea5232c",
|
"packages/ui/components/radio.wrn": "df8e562324b6d99f5a157b0971d63bd715daa0cf6189e7b0d4df04d6f0411388",
|
||||||
"packages/ui/components/select.wrn": "1d8d47a74d5ab9ed58d0ba11f46910897233b96652d17f5962f5dda60bd4cf8a",
|
"packages/ui/components/select.wrn": "747321d6292b8052e44e8014e87ce25f4b56d8e07f86f1061e0ed0cec7651356",
|
||||||
"packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648",
|
"packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648",
|
||||||
"packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105",
|
"packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105",
|
||||||
"packages/ui/components/switch.wrn": "874504e4828e9db6a570d78984c4d3a76d0ceb39d70baa877649cb3798c82c85",
|
"packages/ui/components/switch.wrn": "243f521431ffb644089df7f629bace0c07094638dfdd27e75b2017efa9cbba4f",
|
||||||
"packages/ui/components/textarea.wrn": "9870d37664471a103d44435a3977bf40b087a743acd76ee4d77df9b56cccbb2b",
|
"packages/ui/components/textarea.wrn": "aa3a75214e998a0b57c50e416476d6f85fa0313aaf97262f2f9222150c6c9e26",
|
||||||
"packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d",
|
"packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d",
|
||||||
"packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca",
|
"packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca",
|
||||||
"packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62"
|
"packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62"
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.8.9
|
||||||
|
|
||||||
|
- Added highlighting for the page-level `apis { }` block and its `<name> <METHOD> <path>` entries,
|
||||||
|
along with container and entry completion snippets.
|
||||||
|
- Added `api.` call completion listing every declared block with its method and path, and hover
|
||||||
|
reporting a block's method, path, and request fields.
|
||||||
|
- Taught the language server the `api="…"` binding attribute: it is no longer treated as unknown,
|
||||||
|
and completion inside its quotes offers the page's block names.
|
||||||
|
- Diagnostics for the removed `ssr { … }` / `client { … }` data blocks now name the `apis { }`
|
||||||
|
replacement and land on the offending keyword instead of the start of the file.
|
||||||
|
- Fixed `api.<name>()` calls reporting a spurious "Cannot find name 'api'" error, and the implicit
|
||||||
|
`any` that followed from it.
|
||||||
|
- Restricted API hover to real `api.<name>` references, so a local variable that merely shares a
|
||||||
|
block's name keeps its own hover information.
|
||||||
|
- `client state { }`, `runtime = "client"`, and the `client function` modifier are unaffected —
|
||||||
|
only the `ssr { }` / `client { }` data-block form was removed.
|
||||||
|
|
||||||
## 0.8.8
|
## 0.8.8
|
||||||
|
|
||||||
- Added HTML tag and attribute completions inside WRN `view` blocks, while preserving WRNexus
|
- Added HTML tag and attribute completions inside WRN `view` blocks, while preserving WRNexus
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "wrnexus",
|
"name": "wrnexus",
|
||||||
"displayName": "WRNexus Language Support",
|
"displayName": "WRNexus Language Support",
|
||||||
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
"description": "Complete WRNexus v0.8.3 language support for typed imports, props, state, outputs, runtime functions, stores, diagnostics, formatting, navigation, and migration assistance.",
|
||||||
"version": "0.8.8",
|
"version": "0.8.11",
|
||||||
"publisher": "wrnexus",
|
"publisher": "wrnexus",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
|
|||||||
+361
-40
@@ -1,7 +1,7 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||||
// WRN editor compiler source hash: 2987e8e9d91b793818a195395e740d35d0d5c2a320ff1c3a5ae77363850c7b8e
|
// WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272
|
||||||
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
// WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5
|
||||||
// Generated with TypeScript: 6.0.3
|
// Generated with TypeScript: 6.0.3
|
||||||
const __nodeRequire = require;
|
const __nodeRequire = require;
|
||||||
const __path = __nodeRequire("node:path");
|
const __path = __nodeRequire("node:path");
|
||||||
@@ -1017,6 +1017,27 @@ const HTML_BOOLEAN_ATTRIBUTES = new Set([
|
|||||||
function isHtmlBooleanAttribute(name) {
|
function isHtmlBooleanAttribute(name) {
|
||||||
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* Constraint attributes that must be absent rather than empty. `pattern` is the
|
||||||
|
* dangerous one: an empty pattern compiles to a regex matching only the empty
|
||||||
|
* string, so every typed value becomes invalid and the form silently refuses to
|
||||||
|
* submit. A component declaring `pattern: string = ""` and rendering
|
||||||
|
* `pattern="{pattern}"` therefore breaks every input that did not ask for a
|
||||||
|
* pattern. The rest are inert when empty, but carry no meaning either.
|
||||||
|
*/
|
||||||
|
const OMIT_WHEN_EMPTY_ATTRIBUTES = new Set([
|
||||||
|
"pattern",
|
||||||
|
"minlength",
|
||||||
|
"maxlength",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"step",
|
||||||
|
"inputmode",
|
||||||
|
"accept",
|
||||||
|
]);
|
||||||
|
function isOmitWhenEmptyAttribute(name) {
|
||||||
|
return OMIT_WHEN_EMPTY_ATTRIBUTES.has(name.toLowerCase());
|
||||||
|
}
|
||||||
const URL_ATTRIBUTES = new Set([
|
const URL_ATTRIBUTES = new Set([
|
||||||
"href",
|
"href",
|
||||||
"src",
|
"src",
|
||||||
@@ -1078,9 +1099,13 @@ function renderAttr(attr) {
|
|||||||
case "csrText":
|
case "csrText":
|
||||||
return "";
|
return "";
|
||||||
default:
|
default:
|
||||||
return attr.boolean
|
if (attr.boolean)
|
||||||
? ` ${attr.name}`
|
return ` ${attr.name}`;
|
||||||
: ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
// An empty constraint attribute is worse than absent -- see
|
||||||
|
// OMIT_WHEN_EMPTY_ATTRIBUTES.
|
||||||
|
if (attr.value === "" && isOmitWhenEmptyAttribute(attr.name))
|
||||||
|
return "";
|
||||||
|
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function eventAttribute(name) {
|
function eventAttribute(name) {
|
||||||
@@ -1123,6 +1148,31 @@ function componentEventAttribute(name) {
|
|||||||
}
|
}
|
||||||
return `data-wrn-out-${name}`;
|
return `data-wrn-out-${name}`;
|
||||||
}
|
}
|
||||||
|
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
|
||||||
|
function expandBindings(attrs) {
|
||||||
|
const output = [];
|
||||||
|
for (const attr of attrs) {
|
||||||
|
if (attr.event || !attr.name.startsWith("bind:")) {
|
||||||
|
output.push(attr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target = attr.name.slice("bind:".length);
|
||||||
|
if (target !== "value" && target !== "checked") {
|
||||||
|
throw new Error(`Unknown .wrn binding '${attr.name}'. Use bind:value or bind:checked.`);
|
||||||
|
}
|
||||||
|
const expression = wholeAttributeExpression(attr.value) ?? attr.value.trim();
|
||||||
|
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) {
|
||||||
|
throw new Error(`${attr.name} requires a writable state name, received '${expression}'.`);
|
||||||
|
}
|
||||||
|
output.push({ name: target, value: `{${expression}}`, event: false });
|
||||||
|
output.push({
|
||||||
|
name: target === "checked" ? "change" : "input",
|
||||||
|
value: `${expression} = payload.${target}`,
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
function reactiveAttrValue(raw, reactive) {
|
function reactiveAttrValue(raw, reactive) {
|
||||||
let found = false;
|
let found = false;
|
||||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
||||||
@@ -1141,6 +1191,7 @@ function reactiveAttrValue(raw, reactive) {
|
|||||||
return found ? value : null;
|
return found ? value : null;
|
||||||
}
|
}
|
||||||
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
|
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
|
||||||
|
attrs = expandBindings(attrs);
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const rendered = attrs
|
const rendered = attrs
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
@@ -1266,18 +1317,18 @@ function bakeLoopAttr(raw, typed = false) {
|
|||||||
return out + escLit(attrEscape(raw.slice(last)));
|
return out + escLit(attrEscape(raw.slice(last)));
|
||||||
}
|
}
|
||||||
/** Render one loop-body node to template-literal source (nested loops inline). */
|
/** Render one loop-body node to template-literal source (nested loops inline). */
|
||||||
function renderLoopBody(node) {
|
function renderLoopBody(node, locals = []) {
|
||||||
if (node.type === "text") {
|
if (node.type === "text") {
|
||||||
return bakeLoopText(node.value);
|
return bakeLoopText(node.value);
|
||||||
}
|
}
|
||||||
if (node.type === "each") {
|
if (node.type === "each") {
|
||||||
return compileEachExpr(node);
|
return compileEachExpr(node, locals);
|
||||||
}
|
}
|
||||||
if (node.type === "if") {
|
if (node.type === "if") {
|
||||||
return compileIfExpr(node);
|
return compileIfExpr(node, locals);
|
||||||
}
|
}
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const name = attr.event
|
const name = attr.event
|
||||||
@@ -1291,7 +1342,13 @@ function renderLoopBody(node) {
|
|||||||
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
return escLit(` ${name}="`) + bakeLoopAttr(attr.value, componentTag) + escLit(`"`);
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
const inner = node.children.map(renderLoopBody).join("");
|
// A handler expression is emitted as text and evaluated at event time, so any
|
||||||
|
// `{#each}` variable it names has to travel with the element. The runtime
|
||||||
|
// resolves them with closest("[data-wrn-loop-locals]"). Only elements that
|
||||||
|
// actually bind an event need it -- marking every node would bloat the HTML.
|
||||||
|
const localsAttr = locals.length > 0 && node.attrs.some((attr) => attr.event) ? loopLocalsAttr(locals) : "";
|
||||||
|
const inner = node.children.map((child) => renderLoopBody(child, locals)).join("");
|
||||||
|
const openAttrs = attrs + localsAttr;
|
||||||
if (node.tag === "Static")
|
if (node.tag === "Static")
|
||||||
return inner;
|
return inner;
|
||||||
if (node.tag === "Dynamic")
|
if (node.tag === "Dynamic")
|
||||||
@@ -1335,26 +1392,37 @@ function renderLoopBody(node) {
|
|||||||
if (island)
|
if (island)
|
||||||
return escLit(island);
|
return escLit(island);
|
||||||
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
return (escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
||||||
attrs +
|
openAttrs +
|
||||||
escLit(">") +
|
escLit(">") +
|
||||||
inner +
|
inner +
|
||||||
escLit("</div>"));
|
escLit("</div>"));
|
||||||
}
|
}
|
||||||
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
if (syntax_1.VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
return escLit(`<${node.tag}`) + openAttrs + escLit(">");
|
||||||
}
|
}
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
return escLit(`<${node.tag}`) + openAttrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Build the ` data-wrn-loop-locals="..."` attribute for a page-rendered loop
|
||||||
|
* body. Deliberately not passed through escLit: the `${...}` must stay live so
|
||||||
|
* the values are encoded at render time.
|
||||||
|
*/
|
||||||
|
function loopLocalsAttr(locals) {
|
||||||
|
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
|
||||||
|
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Compile a `{#each list as item}` block to a `${…}` template-literal interpolation
|
* Compile a `{#each list as item}` block to a `${…}` template-literal interpolation
|
||||||
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
|
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
|
||||||
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
|
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
|
||||||
*/
|
*/
|
||||||
function compileEachExpr(node) {
|
function compileEachExpr(node, outerLocals = []) {
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
const index = node.index ?? "__wi";
|
const index = node.index ?? "__wi";
|
||||||
const body = node.body.map(renderLoopBody).join("");
|
// A nested loop can reference the outer loop's variables too.
|
||||||
const empty = node.empty.map(renderLoopBody).join("");
|
const locals = [...outerLocals, item, index];
|
||||||
|
const body = node.body.map((child) => renderLoopBody(child, locals)).join("");
|
||||||
|
const empty = node.empty.map((child) => renderLoopBody(child, outerLocals)).join("");
|
||||||
return ("${(() => { const __wl = Array.isArray(" +
|
return ("${(() => { const __wl = Array.isArray(" +
|
||||||
node.list +
|
node.list +
|
||||||
") ? (" +
|
") ? (" +
|
||||||
@@ -1374,11 +1442,11 @@ function compileEachExpr(node) {
|
|||||||
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
|
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
|
||||||
* Conditions are JS expressions evaluated in the surrounding server scope.
|
* Conditions are JS expressions evaluated in the surrounding server scope.
|
||||||
*/
|
*/
|
||||||
function compileIfExpr(node) {
|
function compileIfExpr(node, locals = []) {
|
||||||
let expr = "``"; // no matching branch → empty string
|
let expr = "``"; // no matching branch → empty string
|
||||||
for (let k = node.branches.length - 1; k >= 0; k--) {
|
for (let k = node.branches.length - 1; k >= 0; k--) {
|
||||||
const b = node.branches[k];
|
const b = node.branches[k];
|
||||||
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`";
|
const bodySrc = "`" + b.body.map((child) => renderLoopBody(child, locals)).join("") + "`";
|
||||||
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
||||||
}
|
}
|
||||||
return "${" + expr + "}";
|
return "${" + expr + "}";
|
||||||
@@ -1520,9 +1588,9 @@ function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindin
|
|||||||
const island = islandMarkerFor(node);
|
const island = islandMarkerFor(node);
|
||||||
if (island)
|
if (island)
|
||||||
return island;
|
return island;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
|
||||||
.join("");
|
.join("");
|
||||||
const inner = node.children
|
const inner = node.children
|
||||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||||
@@ -1535,7 +1603,7 @@ function renderNestedComponentInvocation(node, ctx) {
|
|||||||
if (island)
|
if (island)
|
||||||
return island;
|
return island;
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||||
@@ -2257,6 +2325,18 @@ function generateInner(ast) {
|
|||||||
if (needsRuntimeHelpers) {
|
if (needsRuntimeHelpers) {
|
||||||
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
|
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
|
||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
|
// Loop bodies that bind an event carry their {#each} locals in an encoded
|
||||||
|
// attribute. Emitted only when the view actually produced one, so a page
|
||||||
|
// without handlers in a loop keeps the smaller prelude -- but it MUST be
|
||||||
|
// emitted whenever the marker is, or the render throws on an undefined
|
||||||
|
// function instead of the handler throwing on an undefined variable.
|
||||||
|
if (body.includes("__wrnexusEncodeLoopLocals(")) {
|
||||||
|
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
|
||||||
|
const json = JSON.stringify(value);
|
||||||
|
const buffer = (globalThis as { Buffer?: { from(i: string, e: string): { toString(e: string): string } } }).Buffer;
|
||||||
|
return buffer ? buffer.from(json, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(json)));
|
||||||
|
}`);
|
||||||
|
}
|
||||||
out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`);
|
out.push(`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
}
|
}
|
||||||
if (hasServerApis) {
|
if (hasServerApis) {
|
||||||
@@ -2392,6 +2472,8 @@ ${exposed
|
|||||||
${declarations}
|
${declarations}
|
||||||
${visible.length
|
${visible.length
|
||||||
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
||||||
|
const __response = __values.find((value) => value instanceof Response);
|
||||||
|
if (__response) return __response;
|
||||||
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
|
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
|
||||||
: ""}
|
: ""}
|
||||||
}`;
|
}`;
|
||||||
@@ -2711,7 +2793,7 @@ function renderClientControlTemplate(nodes) {
|
|||||||
}
|
}
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.map((attribute) => {
|
.map((attribute) => {
|
||||||
const name = attribute.event
|
const name = attribute.event
|
||||||
? componentTag
|
? componentTag
|
||||||
@@ -2749,19 +2831,21 @@ function encodeClientControl(value) {
|
|||||||
}
|
}
|
||||||
function renderComponentIfNode(node, ctx) {
|
function renderComponentIfNode(node, ctx) {
|
||||||
let expression = "``";
|
let expression = "``";
|
||||||
|
const clientBranches = [];
|
||||||
for (let index = node.branches.length - 1; index >= 0; index--) {
|
for (let index = node.branches.length - 1; index >= 0; index--) {
|
||||||
const branch = node.branches[index];
|
const branch = node.branches[index];
|
||||||
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
||||||
const bodyExpression = "`" + body + "`";
|
const bodyExpression = "`" + body + "`";
|
||||||
|
clientBranches.unshift(`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`);
|
||||||
expression =
|
expression =
|
||||||
branch.cond === null
|
branch.cond === null
|
||||||
? bodyExpression
|
? bodyExpression
|
||||||
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||||
}
|
}
|
||||||
const definition = encodeClientControl(node.branches.map((branch) => ({
|
// Render all branches on the server into an inert, encoded definition.
|
||||||
cond: branch.cond,
|
// A branch first selected in the browser therefore contains the real nested
|
||||||
body: renderClientControlTemplate(branch.body),
|
// component HTML and scope metadata, not a data-component placeholder.
|
||||||
})));
|
const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
|
||||||
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
||||||
}
|
}
|
||||||
function renderComponentEachNode(node, ctx) {
|
function renderComponentEachNode(node, ctx) {
|
||||||
@@ -2901,7 +2985,7 @@ function renderComponentNode(node, ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
|
const isExplicitComponentMount = node.attrs.some((attribute) => attribute.name === "data-component");
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||||
@@ -2964,6 +3048,18 @@ function renderComponentNode(node, ctx) {
|
|||||||
const compiledValue = isExplicitComponentMount && wholeExpression
|
const compiledValue = isExplicitComponentMount && wholeExpression
|
||||||
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||||
: compileAttrValue(a.value, elementContext);
|
: compileAttrValue(a.value, elementContext);
|
||||||
|
/*
|
||||||
|
* A constraint attribute whose value is only known at render time (a
|
||||||
|
* component prop defaulting to "") must be decided at render time, so
|
||||||
|
* the whole attribute -- name included -- is emitted by the helper.
|
||||||
|
* Component mounts are excluded: there the value is a prop being passed
|
||||||
|
* down, not an HTML attribute being written.
|
||||||
|
*/
|
||||||
|
if (isOmitWhenEmptyAttribute(a.name) && !isExplicitComponentMount) {
|
||||||
|
if (a.value === "")
|
||||||
|
return "";
|
||||||
|
return `\${__wrnOptionalAttr(${JSON.stringify(a.name)}, \`${compiledValue}\`)}`;
|
||||||
|
}
|
||||||
const rendered = ` ${a.name}="${compiledValue}"`;
|
const rendered = ` ${a.name}="${compiledValue}"`;
|
||||||
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
||||||
const referencesLoopVariable = elementContext.loopVars
|
const referencesLoopVariable = elementContext.loopVars
|
||||||
@@ -3264,6 +3360,10 @@ function __wrnHtml(v: unknown): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wrnOptionalAttr(name: string, value: string): string {
|
||||||
|
return value === "" ? "" : " " + name + '="' + value + '"';
|
||||||
|
}
|
||||||
|
|
||||||
function __wrnAttr(v: unknown): string {
|
function __wrnAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
@@ -3343,6 +3443,9 @@ function __wrnRaw(v: unknown): string {
|
|||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
|
out.push(`function __wrnexusEncodeControl(value: unknown): string {
|
||||||
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
|
}`);
|
||||||
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
@@ -3415,6 +3518,9 @@ function wholeAttributeExpression(value) {
|
|||||||
function __wrnHtml(v) {
|
function __wrnHtml(v) {
|
||||||
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : ">");
|
||||||
}
|
}
|
||||||
|
function __wrnOptionalAttr(name, value) {
|
||||||
|
return value === "" ? "" : " " + name + '="' + value + '"';
|
||||||
|
}
|
||||||
function __wrnAttr(v) {
|
function __wrnAttr(v) {
|
||||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """);
|
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """);
|
||||||
}
|
}
|
||||||
@@ -3422,7 +3528,7 @@ function __wrnProp(v) {
|
|||||||
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
||||||
return __wrnAttr(value);
|
return __wrnAttr(value);
|
||||||
}
|
}
|
||||||
function renderPageComponentAttr(attr, dynamicExpressions) {
|
function renderPageComponentAttr(attr, dynamicExpressions, _reactive) {
|
||||||
if (attr.event) {
|
if (attr.event) {
|
||||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||||
}
|
}
|
||||||
@@ -3435,7 +3541,11 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
|
|||||||
}
|
}
|
||||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||||
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||||
return ` ${attr.name}="${marker}"`;
|
// The mount disappears during SSR. Preserve every dynamic component prop
|
||||||
|
// as a parent-owned binding; server-only expressions simply fail closed in
|
||||||
|
// the browser, while page state can continue driving the mounted child.
|
||||||
|
const binding = ` data-wrn-prop-bind-${dynamicExpressions.length - 1}="${attrEscape(JSON.stringify([attr.name, attr.value]))}"`;
|
||||||
|
return ` ${attr.name}="${marker}"${binding}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -3957,6 +4067,7 @@ function islandNamesFrom(imports) {
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.NativeCompileError = void 0;
|
exports.NativeCompileError = void 0;
|
||||||
exports.generateNative = generateNative;
|
exports.generateNative = generateNative;
|
||||||
|
const syntax_1 = require("@wrnexus/syntax");
|
||||||
class NativeCompileError extends Error {
|
class NativeCompileError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(message);
|
super(message);
|
||||||
@@ -4022,12 +4133,12 @@ function expression(value) {
|
|||||||
function textJsx(value) {
|
function textJsx(value) {
|
||||||
const pieces = [];
|
const pieces = [];
|
||||||
let last = 0;
|
let last = 0;
|
||||||
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
for (const segment of (0, syntax_1.extractInterpolations)(value)) {
|
||||||
if (match.index > last)
|
if (segment.start > last)
|
||||||
pieces.push(value.slice(last, match.index));
|
pieces.push(value.slice(last, segment.start));
|
||||||
const expr = match[1].trim();
|
const expr = segment.expression;
|
||||||
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
||||||
last = match.index + match[0].length;
|
last = segment.end;
|
||||||
}
|
}
|
||||||
pieces.push(value.slice(last));
|
pieces.push(value.slice(last));
|
||||||
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
||||||
@@ -6117,6 +6228,26 @@ function expandStructuredStateDeclarations(lines, unit) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
|
||||||
|
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
|
||||||
|
if (!match)
|
||||||
|
return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(match[2].trim());
|
||||||
|
const compact = JSON.stringify(parsed);
|
||||||
|
const indentation = unit.repeat(depth);
|
||||||
|
if (indentation.length + match[1].length + compact.length <= printWidth)
|
||||||
|
return null;
|
||||||
|
const jsonLines = JSON.stringify(parsed, null, unit).split("\n");
|
||||||
|
return [
|
||||||
|
`${indentation}${match[1]}${jsonLines[0]}`,
|
||||||
|
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
function formatWrnPass(source, options = {}) {
|
function formatWrnPass(source, options = {}) {
|
||||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||||
const printWidth = options.printWidth ?? 100;
|
const printWidth = options.printWidth ?? 100;
|
||||||
@@ -6172,6 +6303,12 @@ function formatWrnPass(source, options = {}) {
|
|||||||
index += 1;
|
index += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
|
||||||
|
if (structuredProp) {
|
||||||
|
output.push(...structuredProp);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const leadingClosingBraces = countLeadingClosingBraces(value);
|
const leadingClosingBraces = countLeadingClosingBraces(value);
|
||||||
const closesControlBlock = isControlBlockClose(value);
|
const closesControlBlock = isControlBlockClose(value);
|
||||||
const continuesControlBlock = isControlBlockMiddle(value);
|
const continuesControlBlock = isControlBlockMiddle(value);
|
||||||
@@ -6252,7 +6389,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|||||||
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
|
exports.stripRuntimeFunctionModifiers = exports.parseStructuredImports = exports.parseStoreLifecycle = exports.parseStateDeclarations = exports.parseRuntimeFunctions = exports.parsePersist = exports.parseOutputs = exports.parseComputedDeclarations = exports.supportsSyntaxFeature = exports.diagnosticSummary = exports.sliceSource = exports.createSourceRange = exports.WRN_SYNTAX_FEATURES = exports.WRN_SYNTAX_VERSION = exports.positionAt = exports.isRuntimeTarget = exports.isHydrationStrategy = exports.formatDiagnostic = exports.diagnosticFromError = exports.diagnose = exports.containsReadonlyPropMutation = exports.classifyParseError = exports.assertValidAst = exports.validateTypedInitializer = exports.runtimeTypeOf = exports.inferredRuntimeType = exports.eraseFunctionTypes = exports.extractInterpolations = exports.VOID_ELEMENTS = exports.ParseError = exports.parseHtmlView = exports.parse = exports.formatWrn = exports.skipLiteralOrComment = exports.LexError = exports.Lexer = void 0;
|
||||||
var tokenizer_ts_1 = require("./tokenizer.js");
|
var tokenizer_ts_1 = require("./tokenizer.js");
|
||||||
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
|
Object.defineProperty(exports, "Lexer", { enumerable: true, get: function () { return tokenizer_ts_1.Lexer; } });
|
||||||
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
|
Object.defineProperty(exports, "LexError", { enumerable: true, get: function () { return tokenizer_ts_1.LexError; } });
|
||||||
@@ -6264,6 +6401,8 @@ Object.defineProperty(exports, "parse", { enumerable: true, get: function () { r
|
|||||||
Object.defineProperty(exports, "parseHtmlView", { enumerable: true, get: function () { return parser_ts_1.parseHtmlView; } });
|
Object.defineProperty(exports, "parseHtmlView", { enumerable: true, get: function () { return parser_ts_1.parseHtmlView; } });
|
||||||
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return parser_ts_1.ParseError; } });
|
Object.defineProperty(exports, "ParseError", { enumerable: true, get: function () { return parser_ts_1.ParseError; } });
|
||||||
Object.defineProperty(exports, "VOID_ELEMENTS", { enumerable: true, get: function () { return parser_ts_1.VOID_ELEMENTS; } });
|
Object.defineProperty(exports, "VOID_ELEMENTS", { enumerable: true, get: function () { return parser_ts_1.VOID_ELEMENTS; } });
|
||||||
|
var interpolation_ts_1 = require("./interpolation.js");
|
||||||
|
Object.defineProperty(exports, "extractInterpolations", { enumerable: true, get: function () { return interpolation_ts_1.extractInterpolations; } });
|
||||||
var types_ts_1 = require("./types.js");
|
var types_ts_1 = require("./types.js");
|
||||||
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return types_ts_1.eraseFunctionTypes; } });
|
Object.defineProperty(exports, "eraseFunctionTypes", { enumerable: true, get: function () { return types_ts_1.eraseFunctionTypes; } });
|
||||||
Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return types_ts_1.inferredRuntimeType; } });
|
Object.defineProperty(exports, "inferredRuntimeType", { enumerable: true, get: function () { return types_ts_1.inferredRuntimeType; } });
|
||||||
@@ -6297,6 +6436,50 @@ Object.defineProperty(exports, "parseStoreLifecycle", { enumerable: true, get: f
|
|||||||
Object.defineProperty(exports, "parseStructuredImports", { enumerable: true, get: function () { return v060_ts_1.parseStructuredImports; } });
|
Object.defineProperty(exports, "parseStructuredImports", { enumerable: true, get: function () { return v060_ts_1.parseStructuredImports; } });
|
||||||
Object.defineProperty(exports, "stripRuntimeFunctionModifiers", { enumerable: true, get: function () { return v060_ts_1.stripRuntimeFunctionModifiers; } });
|
Object.defineProperty(exports, "stripRuntimeFunctionModifiers", { enumerable: true, get: function () { return v060_ts_1.stripRuntimeFunctionModifiers; } });
|
||||||
|
|
||||||
|
},
|
||||||
|
"packages/syntax/src/interpolation.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
|
"use strict";
|
||||||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
|
exports.extractInterpolations = extractInterpolations;
|
||||||
|
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
|
||||||
|
function extractInterpolations(value) {
|
||||||
|
const segments = [];
|
||||||
|
let index = 0;
|
||||||
|
while (index < value.length) {
|
||||||
|
if (value[index] !== "{") {
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const start = index++;
|
||||||
|
let depth = 1;
|
||||||
|
let quote = null;
|
||||||
|
while (index < value.length && depth > 0) {
|
||||||
|
const char = value[index];
|
||||||
|
if (quote) {
|
||||||
|
if (char === "\\" && index + 1 < value.length)
|
||||||
|
index += 2;
|
||||||
|
else {
|
||||||
|
if (char === quote)
|
||||||
|
quote = null;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'" || char === "`")
|
||||||
|
quote = char;
|
||||||
|
else if (char === "{")
|
||||||
|
depth++;
|
||||||
|
else if (char === "}")
|
||||||
|
depth--;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (depth === 0) {
|
||||||
|
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
|
"packages/syntax/src/parser.ts": function (module, exports, require, __filename, __dirname) {
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -6484,6 +6667,25 @@ function parse(source) {
|
|||||||
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
|
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
|
||||||
}
|
}
|
||||||
switch (kw.value) {
|
switch (kw.value) {
|
||||||
|
case "auth": {
|
||||||
|
lx.next();
|
||||||
|
expect("eq");
|
||||||
|
const value = lx.next();
|
||||||
|
if (value.type !== "ident" || !["true", "false"].includes(value.value)) {
|
||||||
|
throw new ParseError(`Expected auth = true or false at offset ${value.pos}`);
|
||||||
|
}
|
||||||
|
security.auth = value.value === "true" ? "required" : "false";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "permission": {
|
||||||
|
lx.next();
|
||||||
|
expect("eq");
|
||||||
|
const value = expect("string").value.trim();
|
||||||
|
if (!value)
|
||||||
|
throw new ParseError(`Page permission cannot be empty at offset ${kw.pos}`);
|
||||||
|
security.permission = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "layout": {
|
case "layout": {
|
||||||
// layout = "public" — selects app/layouts/<name>.wrn for this page.
|
// layout = "public" — selects app/layouts/<name>.wrn for this page.
|
||||||
lx.next();
|
lx.next();
|
||||||
@@ -6762,7 +6964,7 @@ function parse(source) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (lx.peek().type === "lbrace") {
|
if (lx.peek().type === "lbrace") {
|
||||||
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`);
|
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`);
|
||||||
}
|
}
|
||||||
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
|
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
|
||||||
}
|
}
|
||||||
@@ -7433,6 +7635,88 @@ exports.isIdentPart = isIdentPart;
|
|||||||
* reimplementing it — a second hand-rolled scanner is how apostrophes in
|
* reimplementing it — a second hand-rolled scanner is how apostrophes in
|
||||||
* prose used to swallow braces.
|
* prose used to swallow braces.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Identifiers that can precede a `/` without ending an expression, so the `/`
|
||||||
|
* opens a regex rather than dividing.
|
||||||
|
*/
|
||||||
|
const REGEX_PRECEDING_KEYWORDS = new Set([
|
||||||
|
"return",
|
||||||
|
"typeof",
|
||||||
|
"instanceof",
|
||||||
|
"in",
|
||||||
|
"of",
|
||||||
|
"new",
|
||||||
|
"delete",
|
||||||
|
"void",
|
||||||
|
"do",
|
||||||
|
"else",
|
||||||
|
"yield",
|
||||||
|
"await",
|
||||||
|
"case",
|
||||||
|
]);
|
||||||
|
/**
|
||||||
|
* Decide whether the `/` at `i` opens a regex literal or is a division sign.
|
||||||
|
*
|
||||||
|
* Scans backwards for the last significant character. `a / b` divides; `(/a/)`,
|
||||||
|
* `= /a/` and `return /a/` do not. Erring towards division is the safe
|
||||||
|
* direction -- mistaking division for a regex would swallow everything to the
|
||||||
|
* next `/` and lose any braces in between.
|
||||||
|
*/
|
||||||
|
function opensRegex(src, i) {
|
||||||
|
let j = i - 1;
|
||||||
|
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === "\n"))
|
||||||
|
j--;
|
||||||
|
if (j < 0)
|
||||||
|
return true;
|
||||||
|
const prev = src[j];
|
||||||
|
if (/[A-Za-z0-9_$]/.test(prev)) {
|
||||||
|
// An identifier ends an expression, so `/` divides -- unless it is a
|
||||||
|
// keyword that cannot end one, like `return`.
|
||||||
|
let k = j;
|
||||||
|
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
|
||||||
|
k--;
|
||||||
|
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
|
||||||
|
}
|
||||||
|
// `)` and `]` close an expression, `.` continues one, and a quote ends a
|
||||||
|
// literal; anything else leaves us in a position where a regex may start.
|
||||||
|
return (prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Scan a regex literal starting at `i`, returning the index just past its
|
||||||
|
* closing `/` and flags, or null when this is not in fact a regex.
|
||||||
|
*
|
||||||
|
* A regex literal cannot span a newline, so an unterminated one is treated as
|
||||||
|
* "not a regex" rather than swallowing the rest of the file. That is what keeps
|
||||||
|
* a bare URL in view text (`https://example.com/a//b`) intact.
|
||||||
|
*/
|
||||||
|
function skipRegex(src, i) {
|
||||||
|
let j = i + 1;
|
||||||
|
let inClass = false;
|
||||||
|
while (j < src.length) {
|
||||||
|
const c = src[j];
|
||||||
|
if (c === "\n")
|
||||||
|
return null;
|
||||||
|
if (c === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inClass) {
|
||||||
|
if (c === "]")
|
||||||
|
inClass = false;
|
||||||
|
}
|
||||||
|
else if (c === "[") {
|
||||||
|
inClass = true;
|
||||||
|
}
|
||||||
|
else if (c === "/") {
|
||||||
|
j++;
|
||||||
|
while (j < src.length && /[a-z]/.test(src[j]))
|
||||||
|
j++;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function skipLiteralOrComment(src, i, atLineStart) {
|
function skipLiteralOrComment(src, i, atLineStart) {
|
||||||
const c = src[i];
|
const c = src[i];
|
||||||
if (c === "/" && src[i + 1] === "*") {
|
if (c === "/" && src[i + 1] === "*") {
|
||||||
@@ -7456,6 +7740,13 @@ function skipLiteralOrComment(src, i, atLineStart) {
|
|||||||
}
|
}
|
||||||
return src.length;
|
return src.length;
|
||||||
}
|
}
|
||||||
|
// A regex literal is neither a string nor a brace pair, but it can contain
|
||||||
|
// both. Without this, a quote inside one opened a phantom string that
|
||||||
|
// swallowed every brace to the next quote, and a lone `{`/`}` miscounted
|
||||||
|
// block depth.
|
||||||
|
if (c === "/" && opensRegex(src, i)) {
|
||||||
|
return skipRegex(src, i);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
class Lexer {
|
class Lexer {
|
||||||
@@ -7464,8 +7755,13 @@ class Lexer {
|
|||||||
constructor(src) {
|
constructor(src) {
|
||||||
this.src = src;
|
this.src = src;
|
||||||
}
|
}
|
||||||
/** Skip whitespace and `// line comments`. */
|
/**
|
||||||
skipTrivia() {
|
* Skip whitespace and `// line comments`, but NOT block comments.
|
||||||
|
*
|
||||||
|
* Kept separate from `skipTrivia` so `startsWithBlockComment` can still see a
|
||||||
|
* block comment that `skipTrivia` would otherwise consume.
|
||||||
|
*/
|
||||||
|
skipWhitespaceAndLineComments() {
|
||||||
const { src } = this;
|
const { src } = this;
|
||||||
while (this.pos < src.length) {
|
while (this.pos < src.length) {
|
||||||
const c = src[this.pos];
|
const c = src[this.pos];
|
||||||
@@ -7481,9 +7777,34 @@ class Lexer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/** True when the next non-trivia characters open a block comment. */
|
/**
|
||||||
|
* Skip whitespace, line comments and block comments.
|
||||||
|
*
|
||||||
|
* Block comments used to be skipped only inside a braced body, so one written
|
||||||
|
* between two members failed with a bare "Unexpected character '/'" -- the
|
||||||
|
* same comment parsed or did not depending on where it sat.
|
||||||
|
*/
|
||||||
|
skipTrivia() {
|
||||||
|
const { src } = this;
|
||||||
|
while (this.pos < src.length) {
|
||||||
|
this.skipWhitespaceAndLineComments();
|
||||||
|
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
|
||||||
|
const close = src.indexOf("*/", this.pos + 2);
|
||||||
|
this.pos = close === -1 ? src.length : close + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* True when the next non-trivia characters open a block comment.
|
||||||
|
*
|
||||||
|
* Deliberately skips only whitespace and line comments: `props {}` refuses
|
||||||
|
* block comments with an explained error, and that check must run before
|
||||||
|
* `skipTrivia` would swallow the comment and drop a declaration silently.
|
||||||
|
*/
|
||||||
startsWithBlockComment() {
|
startsWithBlockComment() {
|
||||||
this.skipTrivia();
|
this.skipWhitespaceAndLineComments();
|
||||||
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
||||||
}
|
}
|
||||||
/** Read and consume the next structural token. */
|
/** Read and consume the next structural token. */
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// WRN editor language server source hash: 70d2454817cc3aa546304c880e0a82313bb73b50051556bf7febd12e6a46c78c
|
// WRN editor language server source hash: 5258b3e76cec47df752d4bb886fbd57a3f3c4af9ca9b6b2bf6742c8d78025ff1
|
||||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||||
// @bun @bun-cjs
|
// @bun @bun-cjs
|
||||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||||
@@ -23,12 +23,14 @@ var __toESM = (mod, isNodeMode, target) => {
|
|||||||
}
|
}
|
||||||
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
||||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||||
for (let key of __getOwnPropNames(mod))
|
if (mod && typeof mod === "object" || typeof mod === "function") {
|
||||||
if (!__hasOwnProp.call(to, key))
|
for (let key of __getOwnPropNames(mod))
|
||||||
__defProp(to, key, {
|
if (!__hasOwnProp.call(to, key))
|
||||||
get: __accessProp.bind(mod, key),
|
__defProp(to, key, {
|
||||||
enumerable: true
|
get: __accessProp.bind(mod, key),
|
||||||
});
|
enumerable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
if (canCache)
|
if (canCache)
|
||||||
cache.set(mod, to);
|
cache.set(mod, to);
|
||||||
return to;
|
return to;
|
||||||
@@ -66,7 +68,7 @@ var __export = (target, all) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js
|
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js
|
||||||
var require_typescript = __commonJS((exports2, module2) => {
|
var require_typescript = __commonJS(function(exports2, module2) {
|
||||||
var __dirname = "E:\\WireJS\\node_modules\\.bun\\typescript@6.0.3\\node_modules\\typescript\\lib", __filename = "E:\\WireJS\\node_modules\\.bun\\typescript@6.0.3\\node_modules\\typescript\\lib\\typescript.js";
|
var __dirname = "E:\\WireJS\\node_modules\\.bun\\typescript@6.0.3\\node_modules\\typescript\\lib", __filename = "E:\\WireJS\\node_modules\\.bun\\typescript@6.0.3\\node_modules\\typescript\\lib\\typescript.js";
|
||||||
/*! *****************************************************************************
|
/*! *****************************************************************************
|
||||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
@@ -169514,7 +169516,7 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|||||||
});
|
});
|
||||||
|
|
||||||
// node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js
|
// node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js
|
||||||
var require_main = __commonJS((exports2, module2) => {
|
var require_main = __commonJS(function(exports2, module2) {
|
||||||
var __defProp2 = Object.defineProperty;
|
var __defProp2 = Object.defineProperty;
|
||||||
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
||||||
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
||||||
@@ -169658,8 +169660,9 @@ var require_main = __commonJS((exports2, module2) => {
|
|||||||
// packages/language-server/src/server.ts
|
// packages/language-server/src/server.ts
|
||||||
var exports_server = {};
|
var exports_server = {};
|
||||||
__export(exports_server, {
|
__export(exports_server, {
|
||||||
|
apiCallCompletions: () => apiCallCompletions,
|
||||||
apiCallHover: () => apiCallHover,
|
apiCallHover: () => apiCallHover,
|
||||||
apiCallCompletions: () => apiCallCompletions
|
isApiReferenceAt: () => isApiReferenceAt
|
||||||
});
|
});
|
||||||
module.exports = __toCommonJS(exports_server);
|
module.exports = __toCommonJS(exports_server);
|
||||||
|
|
||||||
@@ -169670,6 +169673,64 @@ var isWs = (c) => c === " " || c === "\t" || c === `
|
|||||||
` || c === "\r";
|
` || c === "\r";
|
||||||
var isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
var isIdentStart = (c) => /[A-Za-z_]/.test(c);
|
||||||
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
var isIdentPart = (c) => /[A-Za-z0-9_]/.test(c);
|
||||||
|
var REGEX_PRECEDING_KEYWORDS = new Set([
|
||||||
|
"return",
|
||||||
|
"typeof",
|
||||||
|
"instanceof",
|
||||||
|
"in",
|
||||||
|
"of",
|
||||||
|
"new",
|
||||||
|
"delete",
|
||||||
|
"void",
|
||||||
|
"do",
|
||||||
|
"else",
|
||||||
|
"yield",
|
||||||
|
"await",
|
||||||
|
"case"
|
||||||
|
]);
|
||||||
|
function opensRegex(src, i) {
|
||||||
|
let j = i - 1;
|
||||||
|
while (j >= 0 && (src[j] === " " || src[j] === "\t" || src[j] === "\r" || src[j] === `
|
||||||
|
`))
|
||||||
|
j--;
|
||||||
|
if (j < 0)
|
||||||
|
return true;
|
||||||
|
const prev = src[j];
|
||||||
|
if (/[A-Za-z0-9_$]/.test(prev)) {
|
||||||
|
let k = j;
|
||||||
|
while (k >= 0 && /[A-Za-z0-9_$]/.test(src[k]))
|
||||||
|
k--;
|
||||||
|
return REGEX_PRECEDING_KEYWORDS.has(src.slice(k + 1, j + 1));
|
||||||
|
}
|
||||||
|
return prev !== ")" && prev !== "]" && prev !== "." && prev !== '"' && prev !== "'" && prev !== "`";
|
||||||
|
}
|
||||||
|
function skipRegex(src, i) {
|
||||||
|
let j = i + 1;
|
||||||
|
let inClass = false;
|
||||||
|
while (j < src.length) {
|
||||||
|
const c = src[j];
|
||||||
|
if (c === `
|
||||||
|
`)
|
||||||
|
return null;
|
||||||
|
if (c === "\\") {
|
||||||
|
j += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inClass) {
|
||||||
|
if (c === "]")
|
||||||
|
inClass = false;
|
||||||
|
} else if (c === "[") {
|
||||||
|
inClass = true;
|
||||||
|
} else if (c === "/") {
|
||||||
|
j++;
|
||||||
|
while (j < src.length && /[a-z]/.test(src[j]))
|
||||||
|
j++;
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
function skipLiteralOrComment(src, i, atLineStart) {
|
function skipLiteralOrComment(src, i, atLineStart) {
|
||||||
const c = src[i];
|
const c = src[i];
|
||||||
if (c === "/" && src[i + 1] === "*") {
|
if (c === "/" && src[i + 1] === "*") {
|
||||||
@@ -169694,6 +169755,9 @@ function skipLiteralOrComment(src, i, atLineStart) {
|
|||||||
}
|
}
|
||||||
return src.length;
|
return src.length;
|
||||||
}
|
}
|
||||||
|
if (c === "/" && opensRegex(src, i)) {
|
||||||
|
return skipRegex(src, i);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169703,7 +169767,7 @@ class Lexer {
|
|||||||
constructor(src) {
|
constructor(src) {
|
||||||
this.src = src;
|
this.src = src;
|
||||||
}
|
}
|
||||||
skipTrivia() {
|
skipWhitespaceAndLineComments() {
|
||||||
const { src } = this;
|
const { src } = this;
|
||||||
while (this.pos < src.length) {
|
while (this.pos < src.length) {
|
||||||
const c = src[this.pos];
|
const c = src[this.pos];
|
||||||
@@ -169720,8 +169784,20 @@ class Lexer {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
skipTrivia() {
|
||||||
|
const { src } = this;
|
||||||
|
while (this.pos < src.length) {
|
||||||
|
this.skipWhitespaceAndLineComments();
|
||||||
|
if (src[this.pos] === "/" && src[this.pos + 1] === "*") {
|
||||||
|
const close = src.indexOf("*/", this.pos + 2);
|
||||||
|
this.pos = close === -1 ? src.length : close + 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
startsWithBlockComment() {
|
startsWithBlockComment() {
|
||||||
this.skipTrivia();
|
this.skipWhitespaceAndLineComments();
|
||||||
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
return this.src[this.pos] === "/" && this.src[this.pos + 1] === "*";
|
||||||
}
|
}
|
||||||
next() {
|
next() {
|
||||||
@@ -170646,6 +170722,26 @@ function expandStructuredStateDeclarations(lines, unit) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function formatStructuredPropDeclaration(value, unit, depth, printWidth) {
|
||||||
|
const match = /^([A-Za-z_$][\w$]*\??\s*(?::\s*[^=]+?)?\s*=\s*)([\[{][\s\S]*)$/.exec(value);
|
||||||
|
if (!match)
|
||||||
|
return null;
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(match[2].trim());
|
||||||
|
const compact = JSON.stringify(parsed);
|
||||||
|
const indentation = unit.repeat(depth);
|
||||||
|
if (indentation.length + match[1].length + compact.length <= printWidth)
|
||||||
|
return null;
|
||||||
|
const jsonLines = JSON.stringify(parsed, null, unit).split(`
|
||||||
|
`);
|
||||||
|
return [
|
||||||
|
`${indentation}${match[1]}${jsonLines[0]}`,
|
||||||
|
...jsonLines.slice(1).map((jsonLine) => `${indentation}${jsonLine}`)
|
||||||
|
];
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
function formatWrnPass(source, options = {}) {
|
function formatWrnPass(source, options = {}) {
|
||||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||||
const printWidth = options.printWidth ?? 100;
|
const printWidth = options.printWidth ?? 100;
|
||||||
@@ -170702,6 +170798,12 @@ function formatWrnPass(source, options = {}) {
|
|||||||
index += 1;
|
index += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
|
||||||
|
if (structuredProp) {
|
||||||
|
output.push(...structuredProp);
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const leadingClosingBraces = countLeadingClosingBraces(value);
|
const leadingClosingBraces = countLeadingClosingBraces(value);
|
||||||
const closesControlBlock = isControlBlockClose(value);
|
const closesControlBlock = isControlBlockClose(value);
|
||||||
const continuesControlBlock = isControlBlockMiddle(value);
|
const continuesControlBlock = isControlBlockMiddle(value);
|
||||||
@@ -171537,6 +171639,25 @@ function parse(source) {
|
|||||||
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
|
throw new ParseError(`Expected a ${kind} member keyword at offset ${kw.pos}`);
|
||||||
}
|
}
|
||||||
switch (kw.value) {
|
switch (kw.value) {
|
||||||
|
case "auth": {
|
||||||
|
lx.next();
|
||||||
|
expect("eq");
|
||||||
|
const value = lx.next();
|
||||||
|
if (value.type !== "ident" || !["true", "false"].includes(value.value)) {
|
||||||
|
throw new ParseError(`Expected auth = true or false at offset ${value.pos}`);
|
||||||
|
}
|
||||||
|
security.auth = value.value === "true" ? "required" : "false";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "permission": {
|
||||||
|
lx.next();
|
||||||
|
expect("eq");
|
||||||
|
const value = expect("string").value.trim();
|
||||||
|
if (!value)
|
||||||
|
throw new ParseError(`Page permission cannot be empty at offset ${kw.pos}`);
|
||||||
|
security.permission = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "layout": {
|
case "layout": {
|
||||||
lx.next();
|
lx.next();
|
||||||
expect("eq");
|
expect("eq");
|
||||||
@@ -171799,7 +171920,7 @@ function parse(source) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (lx.peek().type === "lbrace") {
|
if (lx.peek().type === "lbrace") {
|
||||||
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }".`);
|
throw new ParseError(`"${rawMode} { … }" data blocks were removed. Declare API calls in a page-level "apis { }" block, and move mode-scoped helpers into "functions { shared function … }" (at offset ${kw.pos}).`);
|
||||||
}
|
}
|
||||||
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
|
throw new ParseError(`Expected a ${rawMode} state block or hydrate assignment`);
|
||||||
}
|
}
|
||||||
@@ -172298,6 +172419,44 @@ function parseHtmlView(src, pos) {
|
|||||||
const nodes = parseNodeList("root");
|
const nodes = parseNodeList("root");
|
||||||
return { nodes, endPos: i };
|
return { nodes, endPos: i };
|
||||||
}
|
}
|
||||||
|
// packages/syntax/src/interpolation.ts
|
||||||
|
function extractInterpolations(value) {
|
||||||
|
const segments = [];
|
||||||
|
let index = 0;
|
||||||
|
while (index < value.length) {
|
||||||
|
if (value[index] !== "{") {
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const start = index++;
|
||||||
|
let depth = 1;
|
||||||
|
let quote = null;
|
||||||
|
while (index < value.length && depth > 0) {
|
||||||
|
const char = value[index];
|
||||||
|
if (quote) {
|
||||||
|
if (char === "\\" && index + 1 < value.length)
|
||||||
|
index += 2;
|
||||||
|
else {
|
||||||
|
if (char === quote)
|
||||||
|
quote = null;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (char === '"' || char === "'" || char === "`")
|
||||||
|
quote = char;
|
||||||
|
else if (char === "{")
|
||||||
|
depth++;
|
||||||
|
else if (char === "}")
|
||||||
|
depth--;
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (depth === 0) {
|
||||||
|
segments.push({ expression: value.slice(start + 1, index - 1).trim(), start, end: index });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
// packages/syntax/src/diagnostics.ts
|
// packages/syntax/src/diagnostics.ts
|
||||||
function stripAsciiControlAndSpace(value) {
|
function stripAsciiControlAndSpace(value) {
|
||||||
let result = "";
|
let result = "";
|
||||||
@@ -172895,7 +173054,7 @@ function runtimeNamespace(runtime) {
|
|||||||
return `__wrn_${runtime}`;
|
return `__wrn_${runtime}`;
|
||||||
}
|
}
|
||||||
function functionDeclaration(fn) {
|
function functionDeclaration(fn) {
|
||||||
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": CustomEvent<any> & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
|
const params = fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": Event & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`).join(", ");
|
||||||
return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
|
return `export ${fn.async ? "async " : ""}function ${fn.name}(${params})${fn.returnType ? `: ${fn.returnType}` : ""} {${fn.body}}`;
|
||||||
}
|
}
|
||||||
function retainedImports(ast) {
|
function retainedImports(ast) {
|
||||||
@@ -172955,6 +173114,27 @@ var RESERVED_BINDING_NAMES = new Set([
|
|||||||
function safeBindingName(name) {
|
function safeBindingName(name) {
|
||||||
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
|
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
|
||||||
}
|
}
|
||||||
|
function safeViewExpression(expression, reservedProps) {
|
||||||
|
if (!reservedProps.size)
|
||||||
|
return expression;
|
||||||
|
const scanner = import_typescript.default.createScanner(import_typescript.default.ScriptTarget.Latest, false, import_typescript.default.LanguageVariant.Standard, expression);
|
||||||
|
let result = "";
|
||||||
|
let copied = 0;
|
||||||
|
for (let token = scanner.scan();token !== import_typescript.default.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
||||||
|
const start = scanner.getTokenPos();
|
||||||
|
const end = scanner.getTextPos();
|
||||||
|
const text = scanner.getTokenText();
|
||||||
|
if (!reservedProps.has(text))
|
||||||
|
continue;
|
||||||
|
const previous = expression.slice(0, start).trimEnd().at(-1);
|
||||||
|
const following = expression.slice(end).trimStart().at(0);
|
||||||
|
if (previous === "." || following === ":")
|
||||||
|
continue;
|
||||||
|
result += expression.slice(copied, start) + `props.${text}`;
|
||||||
|
copied = end;
|
||||||
|
}
|
||||||
|
return result + expression.slice(copied);
|
||||||
|
}
|
||||||
function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
|
function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = findAppRoot(filePath)) {
|
||||||
const ast = parse(source);
|
const ast = parse(source);
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -172995,14 +173175,23 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
|
|||||||
const outputType = ast.outputs.map((output) => `${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`).join("; ");
|
const outputType = ast.outputs.map((output) => `${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void`).join("; ");
|
||||||
const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server");
|
const serverFunctions = ast.runtimeFunctions.filter((fn) => fn.runtime === "server");
|
||||||
const serverType = serverFunctions.map((fn) => `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`).join("; ");
|
const serverType = serverFunctions.map((fn) => `${fn.name}: (${fn.parameters.map((param) => `${param.name}${param.optional ? "?" : ""}: ${param.valueType ?? "unknown"}`).join(", ")}) => ${fn.returnType ?? (fn.async ? "Promise<unknown>" : "unknown")}`).join("; ");
|
||||||
|
const apiType = ast.dataApis.map((block) => {
|
||||||
|
const fields = block.sections ? [...block.sections.parameters, ...block.sections.body] : [];
|
||||||
|
const inputType = fields.length ? `{ ${fields.map((f) => `${f.name}${f.optional ? "?" : ""}: ${f.type}`).join("; ")} }` : "Record<string, never>";
|
||||||
|
return `${block.name}: (input${fields.length ? "" : "?"}: ${inputType}) => Promise<any>`;
|
||||||
|
}).join("; ");
|
||||||
append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
|
append(`declare const output: { [name: string]: (...args: any[]) => void; ${outputType} };`);
|
||||||
for (const output of ast.outputs)
|
for (const output of ast.outputs)
|
||||||
append(`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`);
|
append(`declare const ${output.name}: (${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}) => void;`);
|
||||||
for (const match of source.matchAll(/@event\s+([A-Za-z_$][\w$]*)\s*=\s*function/g))
|
for (const match of source.matchAll(/@event\s+([A-Za-z_$][\w$]*)\s*=\s*function/g))
|
||||||
append(`declare const ${match[1]}: ((...args: unknown[]) => void) | undefined;`);
|
append(`declare const ${match[1]}: ((...args: unknown[]) => void) | undefined;`);
|
||||||
append(`declare const server: { ${serverType} };`);
|
append(`declare const server: { ${serverType} };`);
|
||||||
|
if (ast.dataApis.length)
|
||||||
|
append(`declare const api: { ${apiType} };`);
|
||||||
append(`declare const props: Readonly<${ast.name}Props>;`);
|
append(`declare const props: Readonly<${ast.name}Props>;`);
|
||||||
append("declare const refs: Record<string, Element | null>;");
|
append("declare const refs: Record<string, Element | null>;");
|
||||||
|
append("declare const toast: ((message: string, options?: Record<string, unknown>) => unknown) & { success(message: string, options?: Record<string, unknown>): unknown; error(message: string, options?: Record<string, unknown>): unknown; danger(message: string, options?: Record<string, unknown>): unknown; warning(message: string, options?: Record<string, unknown>): unknown; info(message: string, options?: Record<string, unknown>): unknown; dismiss(id: string | number): void; clear(): void };");
|
||||||
|
append("declare function useFetch(path: string, method?: string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown }, input?: unknown): Promise<any>;");
|
||||||
append(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
|
append(ast.kind === "global-store" || ast.kind === "page-store" ? storeContract(ast) : componentContract(ast));
|
||||||
append(importedWrnDeclarations(ast, filePath, appRoot));
|
append(importedWrnDeclarations(ast, filePath, appRoot));
|
||||||
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
|
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
|
||||||
@@ -173019,6 +173208,53 @@ ${sharedAliases}`);
|
|||||||
append(functionDeclaration(fn), fn.source);
|
append(functionDeclaration(fn), fn.source);
|
||||||
append("}");
|
append("}");
|
||||||
}
|
}
|
||||||
|
const viewFunctions = new Map;
|
||||||
|
for (const fn of ast.runtimeFunctions) {
|
||||||
|
if (fn.runtime !== "server")
|
||||||
|
viewFunctions.set(fn.name, fn.runtime);
|
||||||
|
}
|
||||||
|
for (const [name, runtime] of viewFunctions) {
|
||||||
|
append(`declare const ${name}: typeof ${runtimeNamespace(runtime)}.${name};`);
|
||||||
|
}
|
||||||
|
let bindingIndex = 0;
|
||||||
|
const reservedProps = new Set(ast.props.map((prop) => prop.name).filter((name) => !safeBindingName(name)));
|
||||||
|
const appendViewBindings = (nodes, locals) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.type === "element") {
|
||||||
|
for (const attr of node.attrs) {
|
||||||
|
const expressions = attr.event ? [attr.value] : extractInterpolations(attr.value).map((segment) => segment.expression);
|
||||||
|
for (const expression of expressions) {
|
||||||
|
if (!expression.trim())
|
||||||
|
continue;
|
||||||
|
const declarations = [
|
||||||
|
...[...locals].map((name) => `declare const ${name}: any;`),
|
||||||
|
...attr.event ? [
|
||||||
|
"declare const payload: any;",
|
||||||
|
"declare const event: Event & { target: HTMLElement };"
|
||||||
|
] : []
|
||||||
|
].join(" ");
|
||||||
|
append(`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${safeViewExpression(expression, reservedProps)}); }`, attr.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appendViewBindings(node.children, locals);
|
||||||
|
} else if (node.type === "if") {
|
||||||
|
for (const branch of node.branches) {
|
||||||
|
if (branch.cond)
|
||||||
|
append(`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(branch.cond, reservedProps)}); }`, branch.cond);
|
||||||
|
appendViewBindings(branch.body, locals);
|
||||||
|
}
|
||||||
|
} else if (node.type === "each") {
|
||||||
|
append(`namespace __wrn_view_${bindingIndex++} { void (${safeViewExpression(node.list, reservedProps)}); }`, node.list);
|
||||||
|
const nested = new Set(locals);
|
||||||
|
nested.add(node.item);
|
||||||
|
if (node.index)
|
||||||
|
nested.add(node.index);
|
||||||
|
appendViewBindings(node.body, nested);
|
||||||
|
appendViewBindings(node.empty, locals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
appendViewBindings(ast.view, new Set);
|
||||||
return {
|
return {
|
||||||
ast,
|
ast,
|
||||||
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
|
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
|
||||||
@@ -173124,7 +173360,7 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
|
|||||||
const shape = shapes.get(node.tag);
|
const shape = shapes.get(node.tag);
|
||||||
if (!shape)
|
if (!shape)
|
||||||
return;
|
return;
|
||||||
const attributes = new Map(node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name, attr]));
|
const attributes = new Map(node.attrs.filter((attr) => !attr.event).map((attr) => [attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name, attr]));
|
||||||
const outputNames = new Set(shape.outputs.map((output) => output.name));
|
const outputNames = new Set(shape.outputs.map((output) => output.name));
|
||||||
const position = lineAt(source, `<${node.tag}`);
|
const position = lineAt(source, `<${node.tag}`);
|
||||||
for (const prop of shape.props) {
|
for (const prop of shape.props) {
|
||||||
@@ -173161,7 +173397,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
|
|||||||
}
|
}
|
||||||
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
|
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
|
||||||
continue;
|
continue;
|
||||||
const prop = known.get(attr.name);
|
const propName = attr.name.startsWith("bind:") ? attr.name.slice(5) : attr.name;
|
||||||
|
const prop = known.get(propName);
|
||||||
if (!prop) {
|
if (!prop) {
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
code: "WRN-COMPONENT-UNKNOWN-PROP",
|
code: "WRN-COMPONENT-UNKNOWN-PROP",
|
||||||
@@ -196009,6 +196246,9 @@ function getLanguageService(options = defaultLanguageServiceOptions) {
|
|||||||
findLinkedEditingRanges
|
findLinkedEditingRanges
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function newHTMLDataProvider(id, customData) {
|
||||||
|
return new HTMLDataProvider(id, customData);
|
||||||
|
}
|
||||||
|
|
||||||
// packages/language-server/src/html-regions.ts
|
// packages/language-server/src/html-regions.ts
|
||||||
function viewRegions(text) {
|
function viewRegions(text) {
|
||||||
@@ -196098,7 +196338,13 @@ var semanticTokensLegend2 = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// packages/language-server/src/html-service.ts
|
// packages/language-server/src/html-service.ts
|
||||||
var service = getLanguageService();
|
var wrnexusDataProvider = newHTMLDataProvider("wrnexus", {
|
||||||
|
version: 1,
|
||||||
|
globalAttributes: [
|
||||||
|
{ name: "api", description: "Binds this element to a declared `apis { }` entry." }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
var service = getLanguageService({ customDataProviders: [wrnexusDataProvider] });
|
||||||
function htmlDocument(document) {
|
function htmlDocument(document) {
|
||||||
const virtual = virtualHtmlDocument(document);
|
const virtual = virtualHtmlDocument(document);
|
||||||
return TextDocument2.create(virtual.uri, "html", document.version ?? 1, virtual.text);
|
return TextDocument2.create(virtual.uri, "html", document.version ?? 1, virtual.text);
|
||||||
@@ -196111,9 +196357,18 @@ function markdown(value) {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
function isApiAttributeValuePosition(document, position) {
|
||||||
|
const offset = offsetAt2(document.text, position);
|
||||||
|
if (!isInsideHtml(document, offset))
|
||||||
|
return false;
|
||||||
|
const before = document.text.slice(0, offset);
|
||||||
|
return /\bapi\s*=\s*(["'])(?:(?!\1)[^\n])*$/.test(before);
|
||||||
|
}
|
||||||
function htmlCompletions(document, position) {
|
function htmlCompletions(document, position) {
|
||||||
if (!isInsideHtml(document, offsetAt2(document.text, position)))
|
if (!isInsideHtml(document, offsetAt2(document.text, position)))
|
||||||
return [];
|
return [];
|
||||||
|
if (isApiAttributeValuePosition(document, position))
|
||||||
|
return [];
|
||||||
const virtual = htmlDocument(document);
|
const virtual = htmlDocument(document);
|
||||||
const parsed = service.parseHTMLDocument(virtual);
|
const parsed = service.parseHTMLDocument(virtual);
|
||||||
const list = service.doComplete(virtual, position, parsed);
|
const list = service.doComplete(virtual, position, parsed);
|
||||||
@@ -196243,6 +196498,12 @@ function isApiCallPosition(text, offset) {
|
|||||||
const before = text.slice(0, offset);
|
const before = text.slice(0, offset);
|
||||||
return /\bapi\.\w*$/.test(before);
|
return /\bapi\.\w*$/.test(before);
|
||||||
}
|
}
|
||||||
|
function isApiReferenceAt(text, offset) {
|
||||||
|
let start = offset;
|
||||||
|
while (start > 0 && /\w/.test(text[start - 1]))
|
||||||
|
start--;
|
||||||
|
return /\bapi\.$/.test(text.slice(0, start));
|
||||||
|
}
|
||||||
var documents = new Map;
|
var documents = new Map;
|
||||||
var diagnosticTimers = new Map;
|
var diagnosticTimers = new Map;
|
||||||
var MAX_OPEN_DOCUMENTS = 256;
|
var MAX_OPEN_DOCUMENTS = 256;
|
||||||
@@ -196419,6 +196680,10 @@ async function handle(message) {
|
|||||||
result(message.id, apiCallCompletions(document.text));
|
result(message.id, apiCallCompletions(document.text));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (document && isApiAttributeValuePosition(document, params.position)) {
|
||||||
|
result(message.id, apiCallCompletions(document.text));
|
||||||
|
break;
|
||||||
|
}
|
||||||
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
|
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
|
||||||
const html = document ? htmlCompletions(document, params.position) : [];
|
const html = document ? htmlCompletions(document, params.position) : [];
|
||||||
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);
|
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);
|
||||||
@@ -196437,7 +196702,7 @@ async function handle(message) {
|
|||||||
case "textDocument/hover": {
|
case "textDocument/hover": {
|
||||||
const document = documents.get(params.textDocument.uri);
|
const document = documents.get(params.textDocument.uri);
|
||||||
const word = document ? wordAt(document.text, params.position)?.word : undefined;
|
const word = document ? wordAt(document.text, params.position)?.word : undefined;
|
||||||
const apiHover = word ? apiCallHover(document.text, word) : undefined;
|
const apiHover = word && isApiReferenceAt(document.text, offsetAt(document.text, params.position)) ? apiCallHover(document.text, word) : undefined;
|
||||||
if (apiHover) {
|
if (apiHover) {
|
||||||
result(message.id, { contents: apiHover });
|
result(message.id, { contents: apiHover });
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ test("preserves nested prop defaults while formatting", () => {
|
|||||||
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
|
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2 }), formatted);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("formats large typed JSON prop defaults with the bundled formatter", () => {
|
||||||
|
const source = `component Catalog {
|
||||||
|
props {
|
||||||
|
categories: unknown[] = [{"label":"Platform & Core","apps":[{"label":"Home","href":"/home"},{"label":"SSO","href":"/sso"}]},{"label":"Sales","apps":[{"label":"CRM","href":"/crm"}]}]
|
||||||
|
}
|
||||||
|
view { <div></div> }
|
||||||
|
}`;
|
||||||
|
const formatted = formatWrn(source, { insertSpaces: true, tabSize: 2, printWidth: 70 });
|
||||||
|
|
||||||
|
assert.match(formatted, /categories: unknown\[\] = \[\n \{/);
|
||||||
|
assert.match(formatted, /"apps": \[\n \{/);
|
||||||
|
assert.equal(formatWrn(formatted, { insertSpaces: true, tabSize: 2, printWidth: 70 }), formatted);
|
||||||
|
});
|
||||||
|
|
||||||
test("formats native JSON state values with readable indentation", () => {
|
test("formats native JSON state values with readable indentation", () => {
|
||||||
const source = `component Footer {
|
const source = `component Footer {
|
||||||
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
|
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
import { defineJob } from "@wrnexus/queue";
|
import { defineQueue } from "@wrnexus/queue";
|
||||||
|
|
||||||
export interface WelcomeEmailPayload {
|
export interface WelcomeEmailPayload {
|
||||||
userId: number;
|
userId: number;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineJob<WelcomeEmailPayload>({
|
/*
|
||||||
|
* A file under app/queues must default-export defineQueue(...) -- the loader
|
||||||
|
* refuses anything else rather than registering a queue that would never run.
|
||||||
|
* This example used the older defineJob() shape and so failed to load, which
|
||||||
|
* took the whole example app down with it.
|
||||||
|
*/
|
||||||
|
export default defineQueue({
|
||||||
name: "welcome-email",
|
name: "welcome-email",
|
||||||
async run(job) {
|
jobs: {
|
||||||
console.log(`Welcome email queued for ${job.data.email}`);
|
send: {
|
||||||
|
validate: (data: unknown): data is WelcomeEmailPayload =>
|
||||||
|
typeof data === "object" &&
|
||||||
|
data !== null &&
|
||||||
|
Number.isInteger((data as { userId?: unknown }).userId) &&
|
||||||
|
typeof (data as { email?: unknown }).email === "string",
|
||||||
|
run: async ({ email }: WelcomeEmailPayload) => {
|
||||||
|
console.log(`Welcome email queued for ${email}`);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -55,7 +55,7 @@
|
|||||||
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
||||||
"sbom": "node scripts/generate-sbom.mjs",
|
"sbom": "node scripts/generate-sbom.mjs",
|
||||||
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write",
|
"benchmark:framework": "node --experimental-transform-types scripts/benchmark-framework.mjs --write",
|
||||||
"check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples",
|
"check:production": "bun run check:workspace && bun run check:generated-types && bun run check:public-api && bun run check:ui-visual && bun run validate:0.8 && bun run security:framework && bun run security:asvs && bun run check:editor-compiler && bun run check:editor-language-server && bun run check:editor-extension && bun run check && bun run test:examples && bun run test:services && bun run test:editor",
|
||||||
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
|
"validate:staging": "node --experimental-transform-types scripts/test-package-integrity.mjs",
|
||||||
"stage:packages": "bun run scripts/publish-packages.ts",
|
"stage:packages": "bun run scripts/publish-packages.ts",
|
||||||
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/bun": "^1.3.14",
|
"@types/bun": "^1.4.0",
|
||||||
"@types/react": "^19.2.18",
|
"@types/react": "^19.2.18",
|
||||||
"@types/react-dom": "^19.2.4",
|
"@types/react-dom": "^19.2.4",
|
||||||
"eslint": "^10.8.1",
|
"eslint": "^10.8.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.13",
|
"version": "0.8.18",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts";
|
import { inferIdentityType, normalizeIdentity, publicUser } from "./normalize.ts";
|
||||||
import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts";
|
import { assertPasskeyProvider, MemoryPasskeyChallengeStore } from "./passkeys/index.ts";
|
||||||
import { evaluateAuthRisk } from "./risk.ts";
|
import { evaluateAuthRisk } from "./risk.ts";
|
||||||
|
import { setDefaultAuthEngine } from "./runtime.ts";
|
||||||
import type { AuthStore } from "./store.ts";
|
import type { AuthStore } from "./store.ts";
|
||||||
import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts";
|
import { generateTotpSecret, totpUri, verifyTotp } from "./totp/index.ts";
|
||||||
import type {
|
import type {
|
||||||
@@ -266,7 +267,29 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
if (options.secret.length < MIN_SECRET_LENGTH) {
|
if (options.secret.length < MIN_SECRET_LENGTH) {
|
||||||
throw new TypeError(`auth secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
throw new TypeError(`auth secret must be at least ${MIN_SECRET_LENGTH} characters`);
|
||||||
}
|
}
|
||||||
const store = options.store;
|
let resolvedStore: AuthStore | undefined;
|
||||||
|
const resolveStore = (): AuthStore => {
|
||||||
|
if (resolvedStore) return resolvedStore;
|
||||||
|
resolvedStore = typeof options.store === "function" ? options.store() : options.store;
|
||||||
|
if (!resolvedStore || typeof resolvedStore !== "object") {
|
||||||
|
throw new TypeError("WRN-AUTH-STORE: the auth store factory did not return an AuthStore");
|
||||||
|
}
|
||||||
|
return resolvedStore;
|
||||||
|
};
|
||||||
|
// AuthEngine.store remains source-compatible while deferring the factory
|
||||||
|
// until the first actual property read or method call.
|
||||||
|
const store = new Proxy({} as AuthStore, {
|
||||||
|
get(_target, property) {
|
||||||
|
const value = Reflect.get(resolveStore() as object, property);
|
||||||
|
return typeof value === "function" ? value.bind(resolveStore()) : value;
|
||||||
|
},
|
||||||
|
set(_target, property, value) {
|
||||||
|
return Reflect.set(resolveStore() as object, property, value);
|
||||||
|
},
|
||||||
|
has(_target, property) {
|
||||||
|
return Reflect.has(resolveStore() as object, property);
|
||||||
|
},
|
||||||
|
});
|
||||||
const now = () => {
|
const now = () => {
|
||||||
const value = options.clock?.now() ?? Date.now();
|
const value = options.clock?.now() ?? Date.now();
|
||||||
if (!Number.isFinite(value)) throw new Error("WRN-AUTH-CLOCK: clock returned an invalid time");
|
if (!Number.isFinite(value)) throw new Error("WRN-AUTH-CLOCK: clock returned an invalid time");
|
||||||
@@ -1649,6 +1672,11 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
(typeof metadata.deviceId === "string" ? metadata.deviceId : undefined),
|
(typeof metadata.deviceId === "string" ? metadata.deviceId : undefined),
|
||||||
trusted: Boolean(rememberedDevice) || input.session?.trusted === true,
|
trusted: Boolean(rememberedDevice) || input.session?.trusted === true,
|
||||||
fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined,
|
fingerprint: typeof metadata.fingerprint === "string" ? metadata.fingerprint : undefined,
|
||||||
|
metadata: {
|
||||||
|
...(input.session?.metadata ?? {}),
|
||||||
|
mfaVerifiedAt: now(),
|
||||||
|
mfaMethod: input.method,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
user.lastLoginAt = now();
|
user.lastLoginAt = now();
|
||||||
user.updatedAt = now();
|
user.updatedAt = now();
|
||||||
@@ -2276,5 +2304,9 @@ export function createAuthEngine(options: AuthEngineOptions): AuthEngine {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The application config is evaluated in both dev and generated production
|
||||||
|
// runtimes. Registering here makes that configured engine available to the
|
||||||
|
// package-owned routes without an application startup shim.
|
||||||
|
setDefaultAuthEngine(engine);
|
||||||
return engine;
|
return engine;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
} from "../middleware.ts";
|
} from "../middleware.ts";
|
||||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
||||||
import type { AuthSessionVerificationHandler } from "../types.ts";
|
import type { AuthSessionVerificationHandler } from "../types.ts";
|
||||||
|
import { completeAuth, startAuth, type OAuthProvider } from "@wrnexus/oauth";
|
||||||
|
import { assignDefaultAuthzRoles } from "@wrnexus/authz/defaults";
|
||||||
|
|
||||||
function text(value: unknown): string {
|
function text(value: unknown): string {
|
||||||
return typeof value === "string" ? value : value == null ? "" : String(value);
|
return typeof value === "string" ? value : value == null ? "" : String(value);
|
||||||
@@ -50,6 +52,8 @@ export interface AuthHttpOptions {
|
|||||||
schemas?: AuthSchemaOverrides | AuthSchemaSet;
|
schemas?: AuthSchemaOverrides | AuthSchemaSet;
|
||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
|
oauth?: Record<string, OAuthProvider>;
|
||||||
|
oauthMfaPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
||||||
@@ -59,6 +63,17 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
const onSignedOut = engine.onSignedOut;
|
const onSignedOut = engine.onSignedOut;
|
||||||
const onSuccessfulSignUp = engine.onSuccessfulSignUp;
|
const onSuccessfulSignUp = engine.onSuccessfulSignUp;
|
||||||
|
|
||||||
|
function oauthProvider(ctx: Context): OAuthProvider | undefined {
|
||||||
|
return options.oauth?.[String(ctx.params.provider ?? "").toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function oauthRedirectUri(ctx: Context, provider: OAuthProvider): string {
|
||||||
|
return new URL(
|
||||||
|
`/api/auth/oauth/${encodeURIComponent(provider.name)}/callback`,
|
||||||
|
options.baseUrl ?? ctx.url.origin,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
|
function signupRedirect(ctx: Context, value: string | undefined, fallback: string): Response {
|
||||||
const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
|
const path = safeAuthReturnTo(value, ctx.url.origin) ?? fallback;
|
||||||
return Response.redirect(new URL(path, ctx.url), 303);
|
return Response.redirect(new URL(path, ctx.url), 303);
|
||||||
@@ -80,6 +95,71 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
async oauthProviders(ctx: Context): Promise<Response> {
|
||||||
|
return json({
|
||||||
|
ok: true,
|
||||||
|
providers: Object.values(options.oauth ?? {}).map((provider) => ({
|
||||||
|
id: provider.name,
|
||||||
|
name: provider.name,
|
||||||
|
label: `Continue with ${provider.name.charAt(0).toUpperCase()}${provider.name.slice(1)}`,
|
||||||
|
href: `/api/auth/oauth/${encodeURIComponent(provider.name)}?returnTo=${encodeURIComponent(
|
||||||
|
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ??
|
||||||
|
"/",
|
||||||
|
)}`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async startOAuth(ctx: Context): Promise<Response> {
|
||||||
|
const provider = oauthProvider(ctx);
|
||||||
|
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
|
||||||
|
const returnTo =
|
||||||
|
safeAuthReturnTo(ctx.url.searchParams.get("returnTo") ?? undefined, ctx.url.origin) ?? "/";
|
||||||
|
const started = await startAuth(provider, { redirectUri: oauthRedirectUri(ctx, provider) });
|
||||||
|
ctx.cookies.transaction(`wrnexus.oauth.${provider.name}`).set({
|
||||||
|
state: started.state,
|
||||||
|
verifier: started.verifier,
|
||||||
|
returnTo,
|
||||||
|
});
|
||||||
|
return Response.redirect(started.url, 302);
|
||||||
|
},
|
||||||
|
|
||||||
|
async completeOAuth(ctx: Context): Promise<Response> {
|
||||||
|
const provider = oauthProvider(ctx);
|
||||||
|
if (!provider) return json({ ok: false, error: "OAuth provider not configured" }, 404);
|
||||||
|
const transaction = ctx.cookies
|
||||||
|
.transaction<{
|
||||||
|
state: string;
|
||||||
|
verifier: string;
|
||||||
|
returnTo: string;
|
||||||
|
}>(`wrnexus.oauth.${provider.name}`)
|
||||||
|
.consume();
|
||||||
|
const state = ctx.url.searchParams.get("state");
|
||||||
|
const code = ctx.url.searchParams.get("code");
|
||||||
|
if (!transaction || !state || transaction.state !== state || !code) {
|
||||||
|
return json({ ok: false, error: "OAuth transaction is invalid or expired" }, 400);
|
||||||
|
}
|
||||||
|
const completed = await completeAuth(provider, {
|
||||||
|
code,
|
||||||
|
verifier: transaction.verifier,
|
||||||
|
redirectUri: oauthRedirectUri(ctx, provider),
|
||||||
|
});
|
||||||
|
const result = await engine.loginWithOAuth(
|
||||||
|
provider.name,
|
||||||
|
completed.profile,
|
||||||
|
completed.tokens,
|
||||||
|
);
|
||||||
|
if (result.code === "mfa-required" && result.mfaToken) {
|
||||||
|
ctx.cookies.transaction("wrnexus.auth.mfa", { sameSite: "Lax", maxAge: 300 }).set({
|
||||||
|
mfaToken: result.mfaToken,
|
||||||
|
returnTo: transaction.returnTo,
|
||||||
|
});
|
||||||
|
return Response.redirect(new URL(options.oauthMfaPath ?? "/two-factor", ctx.url), 303);
|
||||||
|
}
|
||||||
|
if (!result.ok || !result.session || !result.user) return json(result, 401);
|
||||||
|
establishAuthSession(ctx, result.session, result.user);
|
||||||
|
if (onSignedIn) return onSignedIn(ctx, transaction.returnTo);
|
||||||
|
return Response.redirect(new URL(transaction.returnTo, ctx.url), 303);
|
||||||
|
},
|
||||||
async verifySession(ctx: Context): Promise<Response> {
|
async verifySession(ctx: Context): Promise<Response> {
|
||||||
const user = getAuthUser(ctx);
|
const user = getAuthUser(ctx);
|
||||||
if (options.onSessionVerification) {
|
if (options.onSessionVerification) {
|
||||||
@@ -110,6 +190,8 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
});
|
});
|
||||||
if (!result.ok || !result.user) return json(result, 400);
|
if (!result.ok || !result.user) return json(result, 400);
|
||||||
|
|
||||||
|
await assignDefaultAuthzRoles(result.user.id, "signup");
|
||||||
|
|
||||||
const action = await onSuccessfulSignUp?.(ctx, result.user);
|
const action = await onSuccessfulSignUp?.(ctx, result.user);
|
||||||
if (action instanceof Response) return action;
|
if (action instanceof Response) return action;
|
||||||
if (action?.autoSignIn) {
|
if (action?.autoSignIn) {
|
||||||
@@ -230,6 +312,7 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
password: text(input.password) || undefined,
|
password: text(input.password) || undefined,
|
||||||
displayName: text(input.displayName) || undefined,
|
displayName: text(input.displayName) || undefined,
|
||||||
});
|
});
|
||||||
|
if (result.ok && result.user) await assignDefaultAuthzRoles(result.user.id, "invitation");
|
||||||
return json(result, result.ok ? 200 : 400);
|
return json(result, result.ok ? 200 : 400);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -403,12 +486,18 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
const validation = await parseBody(schemas.mfaComplete, ctx.req);
|
const validation = await parseBody(schemas.mfaComplete, ctx.req);
|
||||||
if (!validation.ok) return validation.response;
|
if (!validation.ok) return validation.response;
|
||||||
const input = validation.value;
|
const input = validation.value;
|
||||||
|
const continuation = ctx.cookies
|
||||||
|
.transaction<{ mfaToken: string; returnTo?: string }>("wrnexus.auth.mfa", {
|
||||||
|
sameSite: "Lax",
|
||||||
|
maxAge: 300,
|
||||||
|
})
|
||||||
|
.consume();
|
||||||
const methodValue = text(input.method);
|
const methodValue = text(input.method);
|
||||||
const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
|
const method = ["totp", "recovery-code", "email-otp", "sms-otp"].includes(methodValue)
|
||||||
? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
|
? (methodValue as "totp" | "recovery-code" | "email-otp" | "sms-otp")
|
||||||
: "totp";
|
: "totp";
|
||||||
const result = await engine.completeMfa({
|
const result = await engine.completeMfa({
|
||||||
mfaToken: text(input.mfaToken),
|
mfaToken: text(input.mfaToken) || continuation?.mfaToken || "",
|
||||||
method,
|
method,
|
||||||
code: text(input.code),
|
code: text(input.code),
|
||||||
challengeId: text(input.challengeId) || undefined,
|
challengeId: text(input.challengeId) || undefined,
|
||||||
@@ -421,7 +510,10 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
|||||||
if (!result.ok || !result.session || !result.user) return json(result, 400);
|
if (!result.ok || !result.session || !result.user) return json(result, 400);
|
||||||
establishAuthSession(ctx, result.session, result.user);
|
establishAuthSession(ctx, result.session, result.user);
|
||||||
if (onSignedIn) {
|
if (onSignedIn) {
|
||||||
return onSignedIn(ctx, safeAuthReturnTo(text(input.returnTo) || undefined, ctx.url.origin));
|
return onSignedIn(
|
||||||
|
ctx,
|
||||||
|
safeAuthReturnTo(text(input.returnTo) || continuation?.returnTo, ctx.url.origin),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return json(result);
|
return json(result);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export {
|
|||||||
clearAuthSession,
|
clearAuthSession,
|
||||||
getAuthUser,
|
getAuthUser,
|
||||||
getAuthSession,
|
getAuthSession,
|
||||||
|
requireAuthUser,
|
||||||
|
AuthRequiredError,
|
||||||
isAuthenticatedContext,
|
isAuthenticatedContext,
|
||||||
AUTH_SESSION_KEY,
|
AUTH_SESSION_KEY,
|
||||||
} from "./middleware.ts";
|
} from "./middleware.ts";
|
||||||
@@ -20,6 +22,7 @@ export {
|
|||||||
export {
|
export {
|
||||||
authPlugin,
|
authPlugin,
|
||||||
authComponentsDir,
|
authComponentsDir,
|
||||||
|
validateProductionAuthConfig,
|
||||||
type AuthConfig,
|
type AuthConfig,
|
||||||
type AuthRoutesConfig,
|
type AuthRoutesConfig,
|
||||||
type AuthPluginOptions,
|
type AuthPluginOptions,
|
||||||
|
|||||||
@@ -79,6 +79,23 @@ export function getAuthUser(ctx: Context): AuthPublicUser | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Return a typed authenticated user or fail closed for direct handler use. */
|
||||||
|
export function requireAuthUser(ctx: Context): AuthPublicUser {
|
||||||
|
const user = getAuthUser(ctx);
|
||||||
|
if (!user) throw new AuthRequiredError();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthRequiredError extends Error {
|
||||||
|
readonly code = "WRN-AUTH-REQUIRED";
|
||||||
|
readonly status = 401;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super("Authentication is required");
|
||||||
|
this.name = "AuthRequiredError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getAuthSession(ctx: Context): AuthSession | null {
|
export function getAuthSession(ctx: Context): AuthSession | null {
|
||||||
return (ctx.locals.authSession as AuthSession | undefined) ?? null;
|
return (ctx.locals.authSession as AuthSession | undefined) ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ import { readFileSync } from "node:fs";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||||
|
import {
|
||||||
|
discord,
|
||||||
|
github,
|
||||||
|
google,
|
||||||
|
type OAuthProvider,
|
||||||
|
type ProviderCredentials,
|
||||||
|
} from "@wrnexus/oauth";
|
||||||
import type { AuthEngine } from "./engine.ts";
|
import type { AuthEngine } from "./engine.ts";
|
||||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||||
import type { AuthSessionVerificationHandler } from "./types.ts";
|
import type { AuthSessionVerificationHandler } from "./types.ts";
|
||||||
@@ -32,8 +39,11 @@ export interface AuthRoutesConfig {
|
|||||||
sessions?: boolean;
|
sessions?: boolean;
|
||||||
impersonation?: boolean;
|
impersonation?: boolean;
|
||||||
passkeys?: boolean;
|
passkeys?: boolean;
|
||||||
|
oauth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthOAuthProviderConfig = OAuthProvider | ProviderCredentials;
|
||||||
|
|
||||||
export interface AuthConfig {
|
export interface AuthConfig {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
engine?: AuthEngine;
|
engine?: AuthEngine;
|
||||||
@@ -48,12 +58,36 @@ export interface AuthConfig {
|
|||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
csrf?: boolean;
|
csrf?: boolean;
|
||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
|
oauth?: Partial<Record<"google" | "github" | "discord", AuthOAuthProviderConfig>> &
|
||||||
|
Record<string, AuthOAuthProviderConfig>;
|
||||||
|
oauthMfaPath?: string;
|
||||||
|
/** Disable only when an external deployment gate performs equivalent checks. */
|
||||||
|
productionValidation?: boolean;
|
||||||
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
||||||
sessionCookieName?: string;
|
sessionCookieName?: string;
|
||||||
/** Customize the package forward-auth verification response. */
|
/** Customize the package forward-auth verification response. */
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function validateProductionAuthConfig(
|
||||||
|
config: Pick<AuthConfig, "baseUrl" | "oauth">,
|
||||||
|
env: Record<string, string | undefined> = process.env,
|
||||||
|
): void {
|
||||||
|
const secret = env.AUTH_SECRET?.trim() ?? "";
|
||||||
|
if (secret.length < 32 || /^(?:change-me|development|test-only)/i.test(secret)) {
|
||||||
|
throw new Error(
|
||||||
|
"WRN-AUTH-CONFIG: AUTH_SECRET must be a non-development secret of at least 32 characters in production",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!config.baseUrl) throw new Error("WRN-AUTH-CONFIG: auth.baseUrl is required in production");
|
||||||
|
const base = new URL(config.baseUrl);
|
||||||
|
if (base.protocol !== "https:")
|
||||||
|
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must use HTTPS in production");
|
||||||
|
if (["localhost", "127.0.0.1", "::1"].includes(base.hostname)) {
|
||||||
|
throw new Error("WRN-AUTH-CONFIG: auth.baseUrl must not use localhost in production");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
||||||
export interface AuthPluginOptions {
|
export interface AuthPluginOptions {
|
||||||
componentDir?: string;
|
componentDir?: string;
|
||||||
@@ -88,6 +122,8 @@ interface ResolvedAuthConfig {
|
|||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
sessionCookieName?: string;
|
sessionCookieName?: string;
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
|
oauth: Record<string, OAuthProvider>;
|
||||||
|
oauthMfaPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
||||||
@@ -121,6 +157,24 @@ function resolveConfig(
|
|||||||
const enabled = raw.enabled !== false;
|
const enabled = raw.enabled !== false;
|
||||||
const hasEngine = Boolean(raw.engine);
|
const hasEngine = Boolean(raw.engine);
|
||||||
const hasDefaultDb = Boolean(config.db);
|
const hasDefaultDb = Boolean(config.db);
|
||||||
|
const oauth: Record<string, OAuthProvider> = {};
|
||||||
|
for (const [name, provider] of Object.entries(raw.oauth ?? {})) {
|
||||||
|
if (!provider || !provider.clientId?.trim() || !provider.clientSecret?.trim()) continue;
|
||||||
|
oauth[name] =
|
||||||
|
"authorizeUrl" in provider
|
||||||
|
? provider
|
||||||
|
: name === "google"
|
||||||
|
? google(provider)
|
||||||
|
: name === "github"
|
||||||
|
? github(provider)
|
||||||
|
: name === "discord"
|
||||||
|
? discord(provider)
|
||||||
|
: (() => {
|
||||||
|
throw new Error(
|
||||||
|
`WRN-AUTH-OAUTH-CONFIG: custom provider '${name}' requires a complete OAuthProvider`,
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
@@ -148,6 +202,8 @@ function resolveConfig(
|
|||||||
passkey: raw.passkey,
|
passkey: raw.passkey,
|
||||||
sessionCookieName: raw.sessionCookieName,
|
sessionCookieName: raw.sessionCookieName,
|
||||||
onSessionVerification: raw.onSessionVerification,
|
onSessionVerification: raw.onSessionVerification,
|
||||||
|
oauth,
|
||||||
|
oauthMfaPath: raw.oauthMfaPath,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +232,7 @@ function fallbackConfig(options: AuthPluginOptions): ResolvedAuthConfig {
|
|||||||
schemas: resolveAuthSchemas(),
|
schemas: resolveAuthSchemas(),
|
||||||
|
|
||||||
csrf: true,
|
csrf: true,
|
||||||
|
oauth: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,6 +410,16 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
configure(config, context) {
|
configure(config, context) {
|
||||||
|
const raw = (config.auth ?? {}) as AuthConfig;
|
||||||
|
if (
|
||||||
|
context.mode === "production" &&
|
||||||
|
process.env.NODE_ENV === "production" &&
|
||||||
|
raw.enabled !== false &&
|
||||||
|
raw.engine &&
|
||||||
|
raw.productionValidation !== false
|
||||||
|
) {
|
||||||
|
validateProductionAuthConfig(raw);
|
||||||
|
}
|
||||||
const value = resolveConfig(config, options);
|
const value = resolveConfig(config, options);
|
||||||
|
|
||||||
context.metadata.set(resolvedConfigKey, value);
|
context.metadata.set(resolvedConfigKey, value);
|
||||||
@@ -383,6 +450,8 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
|||||||
sessionCookieName: value.sessionCookieName,
|
sessionCookieName: value.sessionCookieName,
|
||||||
|
|
||||||
onSessionVerification: value.onSessionVerification,
|
onSessionVerification: value.onSessionVerification,
|
||||||
|
oauth: value.oauth,
|
||||||
|
oauthMfaPath: value.oauthMfaPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
|
|||||||
baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
|
baseUrl: routeOptions.baseUrl ?? ctx.url.origin,
|
||||||
passkey: routeOptions.passkey,
|
passkey: routeOptions.passkey,
|
||||||
onSessionVerification: routeOptions.onSessionVerification,
|
onSessionVerification: routeOptions.onSessionVerification,
|
||||||
|
oauth: routeOptions.oauth,
|
||||||
|
oauthMfaPath: routeOptions.oauthMfaPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("completeOAuth", ctx);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("startOAuth", ctx);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { invokeAuthHandler } from "../api.ts";
|
||||||
|
|
||||||
|
export function GET(ctx: Context): Promise<Response> {
|
||||||
|
return invokeAuthHandler("oauthProviders", ctx);
|
||||||
|
}
|
||||||
@@ -12,7 +12,8 @@ export type AuthRouteGroup =
|
|||||||
| "mfa"
|
| "mfa"
|
||||||
| "sessions"
|
| "sessions"
|
||||||
| "impersonation"
|
| "impersonation"
|
||||||
| "passkeys";
|
| "passkeys"
|
||||||
|
| "oauth";
|
||||||
|
|
||||||
export interface AuthRouteDefinition {
|
export interface AuthRouteDefinition {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -23,6 +24,24 @@ export interface AuthRouteDefinition {
|
|||||||
|
|
||||||
/** Single source of truth for package-contributed auth endpoints. */
|
/** Single source of truth for package-contributed auth endpoints. */
|
||||||
export const AUTH_ROUTE_DEFINITIONS = [
|
export const AUTH_ROUTE_DEFINITIONS = [
|
||||||
|
{
|
||||||
|
path: "/api/auth/oauth/providers",
|
||||||
|
group: "oauth",
|
||||||
|
handler: "oauthProviders",
|
||||||
|
methods: ["GET"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/api/auth/oauth/[provider]",
|
||||||
|
group: "oauth",
|
||||||
|
handler: "startOAuth",
|
||||||
|
methods: ["GET"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/api/auth/oauth/[provider]/callback",
|
||||||
|
group: "oauth",
|
||||||
|
handler: "completeOAuth",
|
||||||
|
methods: ["GET"],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/api/auth/session/verify",
|
path: "/api/auth/session/verify",
|
||||||
group: "sessions",
|
group: "sessions",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { AuthEngine } from "./engine.ts";
|
import type { AuthEngine } from "./engine.ts";
|
||||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||||
import type { AuthSessionVerificationHandler } from "./types.ts";
|
import type { AuthSessionVerificationHandler } from "./types.ts";
|
||||||
|
import type { OAuthProvider } from "@wrnexus/oauth";
|
||||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
|
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
|
||||||
|
|
||||||
export interface DefaultAuthRouteOptions {
|
export interface DefaultAuthRouteOptions {
|
||||||
@@ -9,6 +10,8 @@ export interface DefaultAuthRouteOptions {
|
|||||||
passkey?: AuthPasskeyHttpOptions;
|
passkey?: AuthPasskeyHttpOptions;
|
||||||
sessionCookieName?: string;
|
sessionCookieName?: string;
|
||||||
onSessionVerification?: AuthSessionVerificationHandler;
|
onSessionVerification?: AuthSessionVerificationHandler;
|
||||||
|
oauth?: Record<string, OAuthProvider>;
|
||||||
|
oauthMfaPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthRuntimeState {
|
interface AuthRuntimeState {
|
||||||
|
|||||||
@@ -403,7 +403,12 @@ export interface AuthRandom {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthEngineOptions {
|
export interface AuthEngineOptions {
|
||||||
store: import("./store.ts").AuthStore;
|
/**
|
||||||
|
* Authentication storage may be created lazily. This is the preferred form
|
||||||
|
* for database-backed stores because application configuration is imported
|
||||||
|
* before the runtime database connection is installed.
|
||||||
|
*/
|
||||||
|
store: import("./store.ts").AuthStore | (() => import("./store.ts").AuthStore);
|
||||||
secret: string;
|
secret: string;
|
||||||
issuer?: string;
|
issuer?: string;
|
||||||
delivery?: AuthDeliveryProvider;
|
delivery?: AuthDeliveryProvider;
|
||||||
@@ -501,7 +506,7 @@ export interface AuthResult {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthenticatedContext extends Context {
|
export interface AuthenticatedContext extends Context<Record<string, string>, AuthPublicUser> {
|
||||||
user: AuthPublicUser;
|
user: AuthPublicUser;
|
||||||
locals: Context["locals"] & {
|
locals: Context["locals"] & {
|
||||||
authUser: AuthPublicUser;
|
authUser: AuthPublicUser;
|
||||||
|
|||||||
@@ -160,11 +160,7 @@ export const mfaOtpRequestSchema = v.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const mfaSchema = v.object({
|
export const mfaSchema = v.object({
|
||||||
mfaToken: v
|
mfaToken: v.string().min(20, "MFA transaction is invalid").max(512).optional(),
|
||||||
.string()
|
|
||||||
.required("MFA transaction is missing")
|
|
||||||
.min(20, "MFA transaction is invalid")
|
|
||||||
.max(512),
|
|
||||||
method: v
|
method: v
|
||||||
.string()
|
.string()
|
||||||
.required("Choose a verification method")
|
.required("Choose a verification method")
|
||||||
|
|||||||
@@ -4,6 +4,25 @@ import { MemoryAuthStore } from "../src/stores/memory.ts";
|
|||||||
import { generateTotp } from "../src/totp/index.ts";
|
import { generateTotp } from "../src/totp/index.ts";
|
||||||
import type { AuthDeliveryMessage, PasskeyProvider } from "../src/types.ts";
|
import type { AuthDeliveryMessage, PasskeyProvider } from "../src/types.ts";
|
||||||
|
|
||||||
|
test("authentication storage factories resolve lazily and only once", async () => {
|
||||||
|
let calls = 0;
|
||||||
|
const backing = new MemoryAuthStore();
|
||||||
|
const engine = createAuthEngine({
|
||||||
|
store: () => {
|
||||||
|
calls++;
|
||||||
|
return backing;
|
||||||
|
},
|
||||||
|
secret: "a secure test secret that is longer than thirty-two characters",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(calls).toBe(0);
|
||||||
|
expect(engine.store).toBeDefined();
|
||||||
|
expect(calls).toBe(0);
|
||||||
|
await engine.getUser("missing");
|
||||||
|
await engine.getUser("still-missing");
|
||||||
|
expect(calls).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
function fixture() {
|
function fixture() {
|
||||||
let time = 1_720_000_000_000;
|
let time = 1_720_000_000_000;
|
||||||
let seed = 11;
|
let seed = 11;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { afterEach, expect, test } from "bun:test";
|
||||||
|
import { createContext, withContextHeaders } from "@wrnexus/core";
|
||||||
|
import type { OAuthProvider } from "@wrnexus/oauth";
|
||||||
|
import { createAuthEngine } from "../src/engine.ts";
|
||||||
|
import { createAuthHttpHandlers } from "../src/http/index.ts";
|
||||||
|
import { MemoryAuthStore } from "../src/stores/memory.ts";
|
||||||
|
|
||||||
|
const realFetch = globalThis.fetch;
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = realFetch;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("configured OAuth owns PKCE state, callback, and session establishment", async () => {
|
||||||
|
const provider: OAuthProvider = {
|
||||||
|
name: "example",
|
||||||
|
authorizeUrl: "https://identity.example/authorize",
|
||||||
|
tokenUrl: "https://identity.example/token",
|
||||||
|
userInfoUrl: "https://identity.example/user",
|
||||||
|
scopes: ["openid", "email"],
|
||||||
|
clientId: "client",
|
||||||
|
clientSecret: "secret",
|
||||||
|
mapProfile: (raw) => ({
|
||||||
|
id: String(raw.id),
|
||||||
|
email: String(raw.email),
|
||||||
|
raw,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const engine = createAuthEngine({
|
||||||
|
store: new MemoryAuthStore(),
|
||||||
|
secret: "oauth-http-secret-that-is-longer-than-thirty-two-characters",
|
||||||
|
});
|
||||||
|
const handlers = createAuthHttpHandlers({
|
||||||
|
engine,
|
||||||
|
baseUrl: "https://app.example",
|
||||||
|
oauth: { example: provider },
|
||||||
|
});
|
||||||
|
const startRequest = new Request(
|
||||||
|
"https://app.example/api/auth/oauth/example?returnTo=%2Fdashboard",
|
||||||
|
);
|
||||||
|
const startCtx = createContext(startRequest, new URL(startRequest.url));
|
||||||
|
startCtx.params = { provider: "example" };
|
||||||
|
const start = await handlers.startOAuth(startCtx);
|
||||||
|
expect(start.status).toBe(302);
|
||||||
|
const location = new URL(start.headers.get("location")!);
|
||||||
|
expect(location.searchParams.get("code_challenge_method")).toBe("S256");
|
||||||
|
const issued = withContextHeaders(startCtx, start).headers.get("set-cookie")!;
|
||||||
|
|
||||||
|
const replies = [
|
||||||
|
Response.json({ access_token: "access", token_type: "Bearer" }),
|
||||||
|
Response.json({ id: "provider-user", email: "oauth@example.test" }),
|
||||||
|
];
|
||||||
|
const mock: typeof fetch = Object.assign(async () => replies.shift()!, {
|
||||||
|
preconnect: () => undefined,
|
||||||
|
});
|
||||||
|
globalThis.fetch = mock;
|
||||||
|
|
||||||
|
const callbackRequest = new Request(
|
||||||
|
`https://app.example/api/auth/oauth/example/callback?code=code&state=${encodeURIComponent(location.searchParams.get("state")!)}`,
|
||||||
|
{ headers: { cookie: issued.split(";", 1)[0]! } },
|
||||||
|
);
|
||||||
|
const callbackCtx = createContext(callbackRequest, new URL(callbackRequest.url));
|
||||||
|
callbackCtx.params = { provider: "example" };
|
||||||
|
const callback = await handlers.completeOAuth(callbackCtx);
|
||||||
|
expect(callback.status).toBe(303);
|
||||||
|
expect(callback.headers.get("location")).toBe("https://app.example/dashboard");
|
||||||
|
expect(callbackCtx.user).toMatchObject({ id: expect.any(String) });
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import type { Context } from "@wrnexus/core";
|
import type { Context } from "@wrnexus/core";
|
||||||
import { createPluginRunner } from "@wrnexus/plugin";
|
import { createPluginRunner } from "@wrnexus/plugin";
|
||||||
import { authPlugin } from "../src/plugin.ts";
|
import { authPlugin, validateProductionAuthConfig } from "../src/plugin.ts";
|
||||||
import { AUTH_ROUTE_DEFINITIONS } from "../src/routes/definitions.ts";
|
import { AUTH_ROUTE_DEFINITIONS } from "../src/routes/definitions.ts";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { createAuthEngine } from "../src/engine.ts";
|
import { createAuthEngine } from "../src/engine.ts";
|
||||||
@@ -65,6 +65,21 @@ test("plugin contributes components, runtime, styles, migration, and toolbar", a
|
|||||||
expect(contributions.middleware).toHaveLength(1);
|
expect(contributions.middleware).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("production auth validation rejects development secrets and origins", () => {
|
||||||
|
expect(() =>
|
||||||
|
validateProductionAuthConfig(
|
||||||
|
{ baseUrl: "http://localhost:3000" },
|
||||||
|
{ AUTH_SECRET: "change-me" },
|
||||||
|
),
|
||||||
|
).toThrow("AUTH_SECRET");
|
||||||
|
expect(() =>
|
||||||
|
validateProductionAuthConfig(
|
||||||
|
{ baseUrl: "http://app.example" },
|
||||||
|
{ AUTH_SECRET: "a-production-secret-that-is-longer-than-thirty-two-characters" },
|
||||||
|
),
|
||||||
|
).toThrow("HTTPS");
|
||||||
|
});
|
||||||
|
|
||||||
test("unconfigured automatic discovery fails closed for routes, middleware, and migrations", async () => {
|
test("unconfigured automatic discovery fails closed for routes, middleware, and migrations", async () => {
|
||||||
const metadata = new Map<string, unknown>();
|
const metadata = new Map<string, unknown>();
|
||||||
const runner = createPluginRunner(authPlugin(), {
|
const runner = createPluginRunner(authPlugin(), {
|
||||||
|
|||||||
@@ -1,15 +1,27 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/authz",
|
"name": "@wrnexus/authz",
|
||||||
"version": "0.8.9",
|
"version": "0.8.15",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./db": "./src/db.ts"
|
"./db": "./src/db.ts",
|
||||||
|
"./plugin": "./src/plugin.ts",
|
||||||
|
"./defaults": "./src/defaults.ts",
|
||||||
|
"./runtime-middleware": "./src/runtime-middleware.ts",
|
||||||
|
"./runtime-memory-middleware": "./src/runtime-memory-middleware.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
"@wrnexus/db": "workspace:*"
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/plugin": "workspace:*"
|
||||||
|
},
|
||||||
|
"wrnexus": {
|
||||||
|
"plugin": {
|
||||||
|
"plugin": "./src/plugin.ts",
|
||||||
|
"export": "default",
|
||||||
|
"factory": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { DecisionPolicy } from "./advanced.ts";
|
|||||||
export interface CatalogSource {
|
export interface CatalogSource {
|
||||||
/** File or package that declared this module, used in conflict messages. */
|
/** File or package that declared this module, used in conflict messages. */
|
||||||
source: string;
|
source: string;
|
||||||
module: AuthzModule;
|
module: AuthzModule<never, never>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Structural equality for declaration metadata. Key order is irrelevant. */
|
/** Structural equality for declaration metadata. Key order is irrelevant. */
|
||||||
|
|||||||
@@ -32,16 +32,26 @@
|
|||||||
|
|
||||||
import type { AuthzCatalog } from "./types.ts";
|
import type { AuthzCatalog } from "./types.ts";
|
||||||
|
|
||||||
let catalog: AuthzCatalog | undefined;
|
interface AuthzCatalogState {
|
||||||
|
catalog?: AuthzCatalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalogKey = Symbol.for("@wrnexus/authz:catalog:v1");
|
||||||
|
|
||||||
|
function state(): AuthzCatalogState {
|
||||||
|
const global = globalThis as Record<PropertyKey, unknown>;
|
||||||
|
return (global[catalogKey] ??= {}) as AuthzCatalogState;
|
||||||
|
}
|
||||||
|
|
||||||
/** Set the process-wide authorization catalog (called by the framework at boot). */
|
/** Set the process-wide authorization catalog (called by the framework at boot). */
|
||||||
export function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog {
|
export function setAuthzCatalog(next: AuthzCatalog): AuthzCatalog {
|
||||||
catalog = next;
|
state().catalog = next;
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The process-wide authorization catalog. Throws if it hasn't been set. */
|
/** The process-wide authorization catalog. Throws if it hasn't been set. */
|
||||||
export function getAuthzCatalog(): AuthzCatalog {
|
export function getAuthzCatalog(): AuthzCatalog {
|
||||||
|
const catalog = state().catalog;
|
||||||
if (!catalog) {
|
if (!catalog) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"WRN-AUTHZ-SETUP: no authorization catalog is configured. The dev server and " +
|
"WRN-AUTHZ-SETUP: no authorization catalog is configured. The dev server and " +
|
||||||
@@ -59,5 +69,5 @@ export function getAuthzCatalog(): AuthzCatalog {
|
|||||||
|
|
||||||
/** Whether the process-wide authorization catalog has been set. */
|
/** Whether the process-wide authorization catalog has been set. */
|
||||||
export function hasAuthzCatalog(): boolean {
|
export function hasAuthzCatalog(): boolean {
|
||||||
return catalog !== undefined;
|
return state().catalog !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import { dbPermissionStore } from "./db.ts";
|
||||||
|
import type { PermissionStore } from "./store.ts";
|
||||||
|
|
||||||
|
export type AuthzIdentityEvent = "signup" | "invitation";
|
||||||
|
|
||||||
|
interface DefaultRoleState {
|
||||||
|
signup: string[];
|
||||||
|
invitation: string[];
|
||||||
|
store?: PermissionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = Symbol.for("@wrnexus/authz:default-roles:v1");
|
||||||
|
|
||||||
|
function state(): DefaultRoleState {
|
||||||
|
const global = globalThis as Record<PropertyKey, unknown>;
|
||||||
|
return (global[key] ??= { signup: [], invitation: [] }) as DefaultRoleState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDefaultAuthzRoles(roles: Partial<DefaultRoleState>): void {
|
||||||
|
state().signup = [...(roles.signup ?? [])];
|
||||||
|
state().invitation = [...(roles.invitation ?? [])];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bind lifecycle role assignment to the same store used by authorization middleware. */
|
||||||
|
export function setDefaultAuthzRoleStore(store: PermissionStore): void {
|
||||||
|
state().store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Assign configured identity lifecycle roles through the framework-owned store. */
|
||||||
|
export async function assignDefaultAuthzRoles(
|
||||||
|
subjectId: string,
|
||||||
|
event: AuthzIdentityEvent,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!subjectId) return;
|
||||||
|
const roles = state()[event];
|
||||||
|
if (!roles.length) return;
|
||||||
|
const store = state().store ?? dbPermissionStore(getDb());
|
||||||
|
await Promise.all(roles.map((role) => store.assignRole(subjectId, role)));
|
||||||
|
}
|
||||||
@@ -157,8 +157,11 @@ export {
|
|||||||
guardPermission,
|
guardPermission,
|
||||||
filterCan,
|
filterCan,
|
||||||
AUTHZ_LOCALS_KEY,
|
AUTHZ_LOCALS_KEY,
|
||||||
|
getRequestAuthorization,
|
||||||
} from "./middleware.ts";
|
} from "./middleware.ts";
|
||||||
export type { GuardOptions } from "./middleware.ts";
|
export type { GuardOptions, AuthorizationResponses, RequestAuthorization } from "./middleware.ts";
|
||||||
|
export { defineAuthorizedHandler, defineOwnedResource } from "./resource.ts";
|
||||||
|
export type { AuthorizedHandlerOptions, OwnedResourceDefinition } from "./resource.ts";
|
||||||
export type {
|
export type {
|
||||||
AuthzScope,
|
AuthzScope,
|
||||||
AuthzCatalog,
|
AuthzCatalog,
|
||||||
|
|||||||
@@ -10,6 +10,27 @@ import type { AuthzScope } from "./types.ts";
|
|||||||
*/
|
*/
|
||||||
export const AUTHZ_LOCALS_KEY = "_authz";
|
export const AUTHZ_LOCALS_KEY = "_authz";
|
||||||
|
|
||||||
|
export interface AuthorizationResponses {
|
||||||
|
forbidden(decision?: AuthorizationDecision, options?: { exposeReason?: boolean }): Response;
|
||||||
|
notFoundOrForbidden(options?: {
|
||||||
|
mayDiscover?: boolean;
|
||||||
|
decision?: AuthorizationDecision;
|
||||||
|
exposeReason?: boolean;
|
||||||
|
}): Response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RequestAuthorization extends AuthorizationResponses {
|
||||||
|
can(permission: string, resource?: unknown): Promise<boolean>;
|
||||||
|
decide(permission: string, resource?: unknown): Promise<AuthorizationDecision>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@wrnexus/core" {
|
||||||
|
interface Context {
|
||||||
|
/** Installed by authzMiddleware for handlers that prefer context-local authorization. */
|
||||||
|
authz?: RequestAuthorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface RequestAuthz {
|
interface RequestAuthz {
|
||||||
resolver: AuthzResolver;
|
resolver: AuthzResolver;
|
||||||
/**
|
/**
|
||||||
@@ -49,10 +70,39 @@ export function authzMiddleware(options: AuthzResolverOptions): Middleware {
|
|||||||
byValue: new Map(),
|
byValue: new Map(),
|
||||||
};
|
};
|
||||||
ctx.locals[AUTHZ_LOCALS_KEY] = request;
|
ctx.locals[AUTHZ_LOCALS_KEY] = request;
|
||||||
|
ctx.authz = {
|
||||||
|
can: (permission, resource) => can(ctx, permission, resource),
|
||||||
|
decide: (permission, resource) => decideFor(ctx, permission, resource),
|
||||||
|
forbidden: (decision, responseOptions) =>
|
||||||
|
forbiddenResponse(decision, responseOptions?.exposeReason),
|
||||||
|
notFoundOrForbidden: (responseOptions = {}) =>
|
||||||
|
responseOptions.mayDiscover
|
||||||
|
? Response.json(
|
||||||
|
{ ok: false, error: "Not Found" },
|
||||||
|
{ status: 404, headers: NO_STORE_HEADERS },
|
||||||
|
)
|
||||||
|
: forbiddenResponse(responseOptions.decision, responseOptions.exposeReason),
|
||||||
|
};
|
||||||
return next();
|
return next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function forbiddenResponse(decision?: AuthorizationDecision, exposeReason = false): Response {
|
||||||
|
return Response.json(
|
||||||
|
exposeReason
|
||||||
|
? { ok: false, error: "Forbidden", reason: decision?.reason, policy: decision?.policy }
|
||||||
|
: { ok: false, error: "Forbidden" },
|
||||||
|
{ status: 403, headers: NO_STORE_HEADERS },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRequestAuthorization(ctx: Context): RequestAuthorization {
|
||||||
|
if (!ctx.authz) {
|
||||||
|
throw new Error("WRN-AUTHZ-SETUP: authzMiddleware() is not registered for this request.");
|
||||||
|
}
|
||||||
|
return ctx.authz;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the tenant from the context at decision time, not at middleware time:
|
* Read the tenant from the context at decision time, not at middleware time:
|
||||||
* a request that switches tenant mid-flight must not keep the old scope.
|
* a request that switches tenant mid-flight must not keep the old scope.
|
||||||
@@ -236,10 +286,7 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
|
|||||||
reason: "Resource unavailable",
|
reason: "Resource unavailable",
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
});
|
});
|
||||||
return Response.json(
|
return forbiddenResponse();
|
||||||
{ ok: false, error: "Forbidden" },
|
|
||||||
{ status: 403, headers: NO_STORE_HEADERS },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,12 +311,7 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json(
|
return forbiddenResponse(result, options.exposeReason);
|
||||||
options.exposeReason
|
|
||||||
? { ok: false, error: "Forbidden", reason: result.reason, policy: result.policy }
|
|
||||||
: { ok: false, error: "Forbidden" },
|
|
||||||
{ status: 403, headers: NO_STORE_HEADERS },
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { basename, dirname, join } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||||
|
import { authzMigrationSql } from "./migrations.ts";
|
||||||
|
import { setDefaultAuthzRoles } from "./defaults.ts";
|
||||||
|
|
||||||
|
export interface AuthzConfig {
|
||||||
|
enabled?: boolean;
|
||||||
|
declarations?: string;
|
||||||
|
store?: "database" | "memory";
|
||||||
|
migrations?: boolean;
|
||||||
|
middleware?: boolean;
|
||||||
|
strict?: boolean;
|
||||||
|
defaultRoles?: {
|
||||||
|
signup?: string[];
|
||||||
|
invitation?: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResolvedAuthzConfig {
|
||||||
|
enabled: boolean;
|
||||||
|
store: "database" | "memory";
|
||||||
|
migrations: boolean;
|
||||||
|
middleware: boolean;
|
||||||
|
strict: boolean;
|
||||||
|
defaultRoles: { signup: string[]; invitation: string[] };
|
||||||
|
driver: "sqlite" | "postgres" | "mysql";
|
||||||
|
}
|
||||||
|
|
||||||
|
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const runtimeExtension = basename(moduleRoot) === "dist" ? ".js" : ".ts";
|
||||||
|
const middlewareEntry = join(moduleRoot, `runtime-middleware${runtimeExtension}`);
|
||||||
|
const memoryMiddlewareEntry = join(moduleRoot, `runtime-memory-middleware${runtimeExtension}`);
|
||||||
|
const metadataKey = "@wrnexus/authz:config";
|
||||||
|
|
||||||
|
function resolved(context: PluginContext): ResolvedAuthzConfig {
|
||||||
|
return (
|
||||||
|
(context.metadata.get(metadataKey) as ResolvedAuthzConfig | undefined) ?? {
|
||||||
|
enabled: false,
|
||||||
|
store: "database",
|
||||||
|
migrations: false,
|
||||||
|
middleware: false,
|
||||||
|
strict: true,
|
||||||
|
defaultRoles: { signup: [], invitation: [] },
|
||||||
|
driver: "sqlite",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authzPlugin() {
|
||||||
|
return definePlugin({
|
||||||
|
name: "@wrnexus/authz",
|
||||||
|
version: "0.8.0",
|
||||||
|
configure(config, context) {
|
||||||
|
const raw = (config.authz ?? {}) as AuthzConfig;
|
||||||
|
const enabled = raw.enabled !== false && Boolean(config.authz);
|
||||||
|
const db = config.db as { driver?: ResolvedAuthzConfig["driver"] } | undefined;
|
||||||
|
const value: ResolvedAuthzConfig = {
|
||||||
|
enabled,
|
||||||
|
store: raw.store ?? "database",
|
||||||
|
migrations: raw.migrations ?? enabled,
|
||||||
|
middleware: raw.middleware ?? enabled,
|
||||||
|
strict: raw.strict ?? true,
|
||||||
|
defaultRoles: {
|
||||||
|
signup: [...(raw.defaultRoles?.signup ?? [])],
|
||||||
|
invitation: [...(raw.defaultRoles?.invitation ?? [])],
|
||||||
|
},
|
||||||
|
driver: db?.driver ?? "sqlite",
|
||||||
|
};
|
||||||
|
if (enabled && value.store === "database" && !config.db) {
|
||||||
|
throw new Error("WRN-AUTHZ-CONFIG: authz.store='database' requires config.db");
|
||||||
|
}
|
||||||
|
context.metadata.set(metadataKey, value);
|
||||||
|
setDefaultAuthzRoles(value.defaultRoles);
|
||||||
|
config.authz = { ...raw, ...value };
|
||||||
|
},
|
||||||
|
middleware(context) {
|
||||||
|
const value = resolved(context);
|
||||||
|
if (!value.enabled || !value.middleware) return [];
|
||||||
|
return [value.store === "database" ? middlewareEntry : memoryMiddlewareEntry];
|
||||||
|
},
|
||||||
|
migrations(context) {
|
||||||
|
const value = resolved(context);
|
||||||
|
if (!value.enabled || !value.migrations || value.store !== "database") return [];
|
||||||
|
const sql = authzMigrationSql(value.driver);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "wrnexus-authz-001",
|
||||||
|
source: `-- +up\n${sql.up.join(";\n")}\n;\n-- +down\n${sql.down.join(";\n")}\n;`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default authzPlugin;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AuthzModule } from "./types.ts";
|
import type { AuthzModule, DefinedAuthzModule } from "./types.ts";
|
||||||
|
|
||||||
const PERMISSION_ID = /^[a-z0-9]+(?::[a-z0-9-]+)+$/;
|
const PERMISSION_ID = /^[a-z0-9]+(?::[a-z0-9-]+)+$/;
|
||||||
|
|
||||||
@@ -6,7 +6,9 @@ const PERMISSION_ID = /^[a-z0-9]+(?::[a-z0-9-]+)+$/;
|
|||||||
* Validate and freeze one authorization declaration. Called from
|
* Validate and freeze one authorization declaration. Called from
|
||||||
* `app/authz/<name>.ts` as the module's default export.
|
* `app/authz/<name>.ts` as the module's default export.
|
||||||
*/
|
*/
|
||||||
export function defineAuthz(module: AuthzModule): AuthzModule {
|
export function defineAuthz<Subject = any, Resource = any>(
|
||||||
|
module: AuthzModule<Subject, Resource>,
|
||||||
|
): DefinedAuthzModule<Subject, Resource> {
|
||||||
const permissions = module.permissions ?? {};
|
const permissions = module.permissions ?? {};
|
||||||
const roles = module.roles ?? {};
|
const roles = module.roles ?? {};
|
||||||
const policies = module.policies ?? {};
|
const policies = module.policies ?? {};
|
||||||
@@ -14,6 +16,11 @@ export function defineAuthz(module: AuthzModule): AuthzModule {
|
|||||||
const bindings = module.bindings ?? {};
|
const bindings = module.bindings ?? {};
|
||||||
|
|
||||||
for (const id of Object.keys(permissions)) {
|
for (const id of Object.keys(permissions)) {
|
||||||
|
if (id.startsWith("role:")) {
|
||||||
|
throw new Error(
|
||||||
|
`WRN-AUTHZ-DECL: permission id '${id}' uses the reserved 'role:' prefix; role grants use 'role:<name>' for inheritance.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (id.includes("*")) {
|
if (id.includes("*")) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`WRN-AUTHZ-DECL: permission id '${id}' must not contain a wildcard; wildcards belong in roles.`,
|
`WRN-AUTHZ-DECL: permission id '${id}' must not contain a wildcard; wildcards belong in roles.`,
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { decideFor } from "./middleware.ts";
|
||||||
|
|
||||||
|
export interface AuthorizedHandlerOptions<Resource, Result extends Response = Response> {
|
||||||
|
permission: string;
|
||||||
|
resource?: (ctx: Context) => Resource | null | undefined | Promise<Resource | null | undefined>;
|
||||||
|
exposeReason?: boolean;
|
||||||
|
mayDiscover?: (ctx: Context) => boolean | Promise<boolean>;
|
||||||
|
handle(ctx: Context & { resource: Resource }): Result | Promise<Result>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Define an API handler whose resource loading and permission check cannot be skipped. */
|
||||||
|
export function defineAuthorizedHandler<Resource = undefined, Result extends Response = Response>(
|
||||||
|
options: AuthorizedHandlerOptions<Resource, Result>,
|
||||||
|
): (ctx: Context) => Promise<Response> {
|
||||||
|
return async (ctx) => {
|
||||||
|
if (!ctx.user) return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
|
||||||
|
const resource = options.resource ? await options.resource(ctx) : (undefined as Resource);
|
||||||
|
if (options.resource && resource == null) {
|
||||||
|
const mayDiscover = await options.mayDiscover?.(ctx);
|
||||||
|
return mayDiscover
|
||||||
|
? Response.json({ ok: false, error: "Not Found" }, { status: 404 })
|
||||||
|
: Response.json({ ok: false, error: "Forbidden" }, { status: 403 });
|
||||||
|
}
|
||||||
|
const decision = await decideFor(ctx, options.permission, resource);
|
||||||
|
if (!decision.allowed) {
|
||||||
|
return ctx.authz!.forbidden(decision, { exposeReason: options.exposeReason });
|
||||||
|
}
|
||||||
|
ctx.resource = resource;
|
||||||
|
return options.handle(ctx as Context & { resource: Resource });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OwnedResourceDefinition<Resource> {
|
||||||
|
name: string;
|
||||||
|
owner(resource: Resource): string | null | undefined;
|
||||||
|
permissions: {
|
||||||
|
read?: string;
|
||||||
|
create?: string;
|
||||||
|
update?: string;
|
||||||
|
delete?: string;
|
||||||
|
readAny?: string;
|
||||||
|
writeAny?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Shared ownership and override vocabulary for application resource modules. */
|
||||||
|
export function defineOwnedResource<Resource>(definition: OwnedResourceDefinition<Resource>) {
|
||||||
|
return Object.freeze({
|
||||||
|
...definition,
|
||||||
|
attributes(resource: Resource) {
|
||||||
|
return { ownerId: definition.owner(resource) };
|
||||||
|
},
|
||||||
|
isOwner(subjectId: string, resource: Resource) {
|
||||||
|
const ownerId = definition.owner(resource);
|
||||||
|
return Boolean(subjectId && ownerId && subjectId === ownerId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { getAuthzCatalog } from "./client.ts";
|
||||||
|
import { setDefaultAuthzRoleStore } from "./defaults.ts";
|
||||||
|
import { authzMiddleware } from "./middleware.ts";
|
||||||
|
import { memoryPermissionStore } from "./store.ts";
|
||||||
|
|
||||||
|
/** Process-local authorization storage for development and stateless tests. */
|
||||||
|
const store = memoryPermissionStore();
|
||||||
|
setDefaultAuthzRoleStore(store);
|
||||||
|
export default function frameworkMemoryAuthz(ctx: Context, next: Next) {
|
||||||
|
return authzMiddleware({ catalog: getAuthzCatalog(), store, strict: true })(ctx, next);
|
||||||
|
}
|
||||||
|
import type { Context, Next } from "@wrnexus/core";
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Context, Next } from "@wrnexus/core";
|
||||||
|
import { getDb, type Db } from "@wrnexus/db";
|
||||||
|
import { getAuthzCatalog } from "./client.ts";
|
||||||
|
import { dbPermissionStore, ensureAuthzTables } from "./db.ts";
|
||||||
|
import { setDefaultAuthzRoleStore } from "./defaults.ts";
|
||||||
|
import { authzMiddleware } from "./middleware.ts";
|
||||||
|
|
||||||
|
const initialized = new WeakMap<Db, Promise<void>>();
|
||||||
|
|
||||||
|
function initialize(db: Db): Promise<void> {
|
||||||
|
let pending = initialized.get(db);
|
||||||
|
if (!pending) {
|
||||||
|
pending = ensureAuthzTables(db);
|
||||||
|
initialized.set(db, pending);
|
||||||
|
pending.catch(() => initialized.delete(db));
|
||||||
|
}
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Package-installed authorization middleware with lazy database resolution. */
|
||||||
|
export default async function frameworkAuthz(ctx: Context, next: Next): Promise<Response> {
|
||||||
|
const db = getDb();
|
||||||
|
await initialize(db);
|
||||||
|
const store = dbPermissionStore(db);
|
||||||
|
setDefaultAuthzRoleStore(store);
|
||||||
|
return authzMiddleware({
|
||||||
|
catalog: getAuthzCatalog(),
|
||||||
|
store,
|
||||||
|
strict: true,
|
||||||
|
})(ctx, next);
|
||||||
|
}
|
||||||
@@ -18,15 +18,24 @@ export interface AttributeMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** One `app/authz/<name>.ts` declaration. */
|
/** One `app/authz/<name>.ts` declaration. */
|
||||||
export interface AuthzModule {
|
export interface AuthzModule<Subject = any, Resource = any> {
|
||||||
permissions?: Record<string, PermissionMeta>;
|
permissions?: Record<string, PermissionMeta>;
|
||||||
roles?: Record<string, string[]>;
|
roles?: Record<string, string[]>;
|
||||||
policies?: Record<string, DecisionPolicy<never, never>>;
|
policies?: Record<string, DecisionPolicy<Subject, Resource>>;
|
||||||
attributes?: Record<string, AttributeMeta>;
|
attributes?: Record<string, AttributeMeta>;
|
||||||
/** permission id -> policy names that must pass for it. */
|
/** permission id -> policy names that must pass for it. */
|
||||||
bindings?: Record<string, string[]>;
|
bindings?: Record<string, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A declaration after defineAuthz() has supplied all optional collections. */
|
||||||
|
export type DefinedAuthzModule<Subject = any, Resource = any> = {
|
||||||
|
permissions: Record<string, PermissionMeta>;
|
||||||
|
roles: Record<string, string[]>;
|
||||||
|
policies: Record<string, DecisionPolicy<Subject, Resource>>;
|
||||||
|
attributes: Record<string, AttributeMeta>;
|
||||||
|
bindings: Record<string, string[]>;
|
||||||
|
};
|
||||||
|
|
||||||
/** The merged, frozen view of every declaration in the app. */
|
/** The merged, frozen view of every declaration in the app. */
|
||||||
export interface AuthzCatalog {
|
export interface AuthzCatalog {
|
||||||
permissions: ReadonlyMap<string, PermissionMeta>;
|
permissions: ReadonlyMap<string, PermissionMeta>;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { createPluginRunner } from "@wrnexus/plugin";
|
||||||
|
import { authzPlugin } from "../src/plugin.ts";
|
||||||
|
|
||||||
|
function runner() {
|
||||||
|
return createPluginRunner(authzPlugin(), {
|
||||||
|
root: process.cwd(),
|
||||||
|
mode: "development",
|
||||||
|
command: "dev",
|
||||||
|
metadata: new Map(),
|
||||||
|
warn() {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("authz plugin is inert until configured", async () => {
|
||||||
|
const instance = runner();
|
||||||
|
await instance.configure({});
|
||||||
|
const contributions = await instance.contributions();
|
||||||
|
expect(contributions.middleware).toEqual([]);
|
||||||
|
expect(contributions.migrations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("database authz config contributes middleware and dialect migration", async () => {
|
||||||
|
const instance = runner();
|
||||||
|
const config: Record<string, unknown> = {
|
||||||
|
db: { driver: "postgres", url: "postgres://example" },
|
||||||
|
authz: {
|
||||||
|
store: "database",
|
||||||
|
middleware: true,
|
||||||
|
migrations: true,
|
||||||
|
defaultRoles: { signup: ["manager"] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await instance.configure(config);
|
||||||
|
const contributions = await instance.contributions();
|
||||||
|
expect(contributions.middleware).toHaveLength(1);
|
||||||
|
expect(contributions.migrations).toHaveLength(1);
|
||||||
|
expect(contributions.migrations[0]?.source).toContain("SERIAL PRIMARY KEY");
|
||||||
|
expect(config.authz).toMatchObject({ strict: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("database authz config refuses a missing database", async () => {
|
||||||
|
const instance = runner();
|
||||||
|
await expect(instance.configure({ authz: { store: "database" } })).rejects.toThrow(
|
||||||
|
"requires config.db",
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -26,6 +26,12 @@ describe("defineAuthz", () => {
|
|||||||
expect(() => defineAuthz({ permissions: { "post:*": {} } })).toThrow(/wildcard/i);
|
expect(() => defineAuthz({ permissions: { "post:*": {} } })).toThrow(/wildcard/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rejects permission ids that collide with role inheritance", () => {
|
||||||
|
expect(() => defineAuthz({ permissions: { "role:assign": {} } })).toThrow(
|
||||||
|
/reserved 'role:' prefix/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("rejects a role granting an unknown-shaped entry", () => {
|
test("rejects a role granting an unknown-shaped entry", () => {
|
||||||
expect(() => defineAuthz({ roles: { editor: [""] } })).toThrow(/role 'editor'/i);
|
expect(() => defineAuthz({ roles: { editor: [""] } })).toThrow(/role 'editor'/i);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/captcha",
|
"name": "@wrnexus/captcha",
|
||||||
"version": "0.8.11",
|
"version": "0.8.14",
|
||||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import {
|
||||||
|
captchaContext,
|
||||||
|
captchaResultResponse,
|
||||||
|
captchaTokenFrom,
|
||||||
|
verifyCaptchaOrThrow,
|
||||||
|
} from "../src/helpers.ts";
|
||||||
|
|
||||||
|
describe("CAPTCHA helper boundaries", () => {
|
||||||
|
test("extracts JSON request fallbacks without consuming the caller's request", async () => {
|
||||||
|
const request = new Request("https://example.test/verify", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ captchaToken: 12345 }),
|
||||||
|
});
|
||||||
|
expect(await captchaTokenFrom(request)).toBe("12345");
|
||||||
|
expect(await request.json()).toEqual({ captchaToken: 12345 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("turns verification failures into stable errors and no-store responses", async () => {
|
||||||
|
const provider = {
|
||||||
|
name: "custom" as const,
|
||||||
|
client: { responseField: "captchaToken" },
|
||||||
|
verify: async () => ({
|
||||||
|
success: false as const,
|
||||||
|
code: "expired",
|
||||||
|
message: "Try again",
|
||||||
|
provider: "custom" as const,
|
||||||
|
action: "login",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
await expect(
|
||||||
|
verifyCaptchaOrThrow(provider, { providerToken: "x", action: "login" }),
|
||||||
|
).rejects.toThrow("WRN-CAPTCHA-EXPIRED: Try again");
|
||||||
|
const response = captchaResultResponse({
|
||||||
|
success: false,
|
||||||
|
code: "invalid",
|
||||||
|
provider: "custom",
|
||||||
|
action: "login",
|
||||||
|
});
|
||||||
|
expect(response.status).toBe(403);
|
||||||
|
expect(response.headers.get("cache-control")).toBe("no-store");
|
||||||
|
expect(
|
||||||
|
captchaContext({
|
||||||
|
locals: { captcha: { success: true, provider: "custom", action: "login" } },
|
||||||
|
} as never),
|
||||||
|
).toEqual({ success: true, provider: "custom", action: "login" });
|
||||||
|
expect(captchaContext({ locals: { captcha: "forged" } } as never)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("does not convert provider transport failures into successful verification", async () => {
|
||||||
|
const provider = {
|
||||||
|
name: "custom" as const,
|
||||||
|
client: { responseField: "captchaToken" },
|
||||||
|
verify: async () => {
|
||||||
|
throw new Error("provider timeout");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await expect(
|
||||||
|
verifyCaptchaOrThrow(provider, { providerToken: "x", action: "checkout" }),
|
||||||
|
).rejects.toThrow("provider timeout");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.47",
|
"version": "0.8.64",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./workspace": "./src/workspace.ts"
|
"./workspace": "./src/workspace.ts",
|
||||||
|
"./test-setup": "./src/test-setup.ts"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts"
|
"wrnexus": "src/index.ts"
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { runGenerateResource } from "./generate.ts";
|
||||||
|
|
||||||
|
const FEATURES: Record<string, Record<string, string>> = {
|
||||||
|
queue: {
|
||||||
|
"app/queues/example.ts": `import { defineQueue } from "@wrnexus/queue";\n\nexport default defineQueue({\n name: "example",\n jobs: {\n run: {\n run: async (data: { id: string }) => { console.log("job", data.id); },\n success: async (data) => { console.log("completed", data.id); },\n failed: async (data, error) => { console.error("failed", data.id, error); },\n },\n },\n});\n`,
|
||||||
|
},
|
||||||
|
mail: {
|
||||||
|
"app/lib/mail.ts": `import { defineMail } from "@wrnexus/mail";\n\nexport const mail = defineMail({\n from: process.env.MAIL_FROM,\n driver: { send: async () => { throw new Error("Configure a mail driver"); } },\n sandbox: { enabled: process.env.NODE_ENV !== "production", allowlist: [] },\n});\n`,
|
||||||
|
},
|
||||||
|
seed: {
|
||||||
|
"app/db/seed.ts": `import { addSeedData, defineSeed } from "@wrnexus/db";\n\nexport default defineSeed(async (db) => {\n await addSeedData(db, { name: "Example" }, "examples", { conflict: "ignore" });\n});\n`,
|
||||||
|
},
|
||||||
|
testing: {
|
||||||
|
"test/app.test.ts": `import { expect, test } from "bun:test";\nimport { createTestApp } from "@wrnexus/test";\n\ntest("app boots", async () => {\n const app = await createTestApp(import.meta.dir + "/..");\n expect((await app.fetch("/healthz")).ok).toBe(true);\n app.close();\n});\n`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function runAdd(root: string, feature: string | undefined, name?: string) {
|
||||||
|
if (feature === "resource") return runGenerateResource(root, name);
|
||||||
|
const files = feature ? FEATURES[feature] : undefined;
|
||||||
|
if (!files)
|
||||||
|
throw new Error(`Use wrnexus add ${Object.keys(FEATURES).join(" | ")} | resource <name>`);
|
||||||
|
for (const [relative, content] of Object.entries(files)) {
|
||||||
|
const target = join(resolve(root), relative);
|
||||||
|
if (existsSync(target)) {
|
||||||
|
console.log(`• ${relative} exists — skipped`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
mkdirSync(dirname(target), { recursive: true });
|
||||||
|
writeFileSync(target, content, "utf8");
|
||||||
|
console.log(`✓ Added ${relative}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -702,6 +702,15 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
|
|||||||
imports.push(`import "./.authz-setup.ts";`);
|
imports.push(`import "./.authz-setup.ts";`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runtime plugin state (notably the configured auth engine and OAuth
|
||||||
|
// providers) is established by evaluating the application config. The build
|
||||||
|
// process runs in a different process, so its globals cannot serve the
|
||||||
|
// generated production server.
|
||||||
|
const runtimeConfigPath = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]
|
||||||
|
.map((name) => resolve(appRoot, name))
|
||||||
|
.find(existsSync);
|
||||||
|
if (runtimeConfigPath) imports.push(`import ${JSON.stringify(fwd(runtimeConfigPath))};`);
|
||||||
|
|
||||||
const manifestRoutes = (routes: Route[]): string => {
|
const manifestRoutes = (routes: Route[]): string => {
|
||||||
const parts = routes.map((r) => {
|
const parts = routes.map((r) => {
|
||||||
const v = `m${counter++}`;
|
const v = `m${counter++}`;
|
||||||
@@ -754,6 +763,15 @@ applyAuthzManifestEarly([${authzSetupEntries}]);
|
|||||||
.join(", ");
|
.join(", ");
|
||||||
if (router.services.length) console.log(`✓ RPC services: ${router.services.length}`);
|
if (router.services.length) console.log(`✓ RPC services: ${router.services.length}`);
|
||||||
|
|
||||||
|
const queuesLit = (router.queues ?? [])
|
||||||
|
.map((queue) => {
|
||||||
|
const v = `q${counter++}`;
|
||||||
|
imports.push(`import * as ${v} from ${JSON.stringify(fwd(queue.file))};`);
|
||||||
|
return `{ name: ${JSON.stringify(queue.name)}, mod: ${v} }`;
|
||||||
|
})
|
||||||
|
.join(", ");
|
||||||
|
if (router.queues?.length) console.log(`✓ Queues: ${router.queues.length}`);
|
||||||
|
|
||||||
// Authorization declarations again, this time for ProdOptions.authz — a
|
// Authorization declarations again, this time for ProdOptions.authz — a
|
||||||
// SEPARATE set of static imports of the exact same files (harmless; ES
|
// SEPARATE set of static imports of the exact same files (harmless; ES
|
||||||
// modules are evaluated once and shared across every importer), statically
|
// modules are evaluated once and shared across every importer), statically
|
||||||
@@ -789,6 +807,7 @@ await createProductionServer(
|
|||||||
components: [${componentsLit}],
|
components: [${componentsLit}],
|
||||||
layouts: [${layoutsLit}],
|
layouts: [${layoutsLit}],
|
||||||
services: [${servicesLit}],
|
services: [${servicesLit}],
|
||||||
|
queues: [${queuesLit}],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
reactivePath: join(import.meta.dir, "reactive.js"),
|
reactivePath: join(import.meta.dir, "reactive.js"),
|
||||||
@@ -806,6 +825,7 @@ await createProductionServer(
|
|||||||
i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
|
i18n: ${i18n ? JSON.stringify(i18n) : "undefined"},
|
||||||
db: ${config.db ? JSON.stringify(config.db) : "undefined"},
|
db: ${config.db ? JSON.stringify(config.db) : "undefined"},
|
||||||
databases: ${config.databases ? JSON.stringify(config.databases) : "undefined"},
|
databases: ${config.databases ? JSON.stringify(config.databases) : "undefined"},
|
||||||
|
queue: ${config.queue ? JSON.stringify(config.queue) : "undefined"},
|
||||||
storage: ${config.storage ? JSON.stringify(config.storage) : "undefined"},
|
storage: ${config.storage ? JSON.stringify(config.storage) : "undefined"},
|
||||||
${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""}
|
${hasDefaultMigrations ? `migrationsDir: join(import.meta.dir, "migrations"),` : ""}
|
||||||
${
|
${
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { runBuild } from "./build.ts";
|
||||||
|
import { runTypecheck } from "./types.ts";
|
||||||
|
import { assertReachability } from "./reachability.ts";
|
||||||
|
|
||||||
|
async function runScript(root: string, name: string): Promise<void> {
|
||||||
|
const child = Bun.spawn(["bun", "run", name], {
|
||||||
|
cwd: root,
|
||||||
|
stdin: "inherit",
|
||||||
|
stdout: "inherit",
|
||||||
|
stderr: "inherit",
|
||||||
|
});
|
||||||
|
const code = await child.exited;
|
||||||
|
if (code !== 0) throw new Error(`WRN-CHECK: '${name}' failed with exit code ${code}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Canonical generated-artifact-aware application verification pipeline. */
|
||||||
|
export async function runCheck(appRoot: string): Promise<void> {
|
||||||
|
const root = resolve(appRoot);
|
||||||
|
const manifest = JSON.parse(readFileSync(resolve(root, "package.json"), "utf8")) as {
|
||||||
|
scripts?: Record<string, string>;
|
||||||
|
};
|
||||||
|
assertReachability(root);
|
||||||
|
await runBuild(root);
|
||||||
|
if (manifest.scripts?.typecheck) await runScript(root, "typecheck");
|
||||||
|
else if (!(await runTypecheck(root))) throw new Error("WRN-CHECK: application typecheck failed");
|
||||||
|
for (const name of ["lint", "test", "format:check"] as const) {
|
||||||
|
if (manifest.scripts?.[name]) await runScript(root, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { runAdd } from "./add.ts";
|
||||||
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
|
import { AI_GUIDE, CLAUDE_MD } from "./ai-guide.ts";
|
||||||
import { currentCliVersion } from "./update-notifier.ts";
|
import { currentCliVersion } from "./update-notifier.ts";
|
||||||
|
|
||||||
@@ -93,7 +94,7 @@ Thumbs.db
|
|||||||
"doctor": "wrnexus doctor .",
|
"doctor": "wrnexus doctor .",
|
||||||
"analyze": "wrnexus analyze .",
|
"analyze": "wrnexus analyze .",
|
||||||
"inspect": "wrnexus inspect packages .",
|
"inspect": "wrnexus inspect packages .",
|
||||||
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check"
|
"check": "wrnexus check ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/ai": "${scaffoldFrameworkRange}",
|
"@wrnexus/ai": "${scaffoldFrameworkRange}",
|
||||||
@@ -227,8 +228,13 @@ export default tseslint.config(
|
|||||||
dist/
|
dist/
|
||||||
.wrnexus/
|
.wrnexus/
|
||||||
**/.wrnexus/
|
**/.wrnexus/
|
||||||
|
.superpowers/
|
||||||
|
.claude/
|
||||||
*.log
|
*.log
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
app/routes.gen.ts
|
||||||
|
app/types/*.generated.*
|
||||||
|
app/db/queries.gen.ts
|
||||||
`,
|
`,
|
||||||
".editorconfig": `root = true
|
".editorconfig": `root = true
|
||||||
|
|
||||||
@@ -667,13 +673,26 @@ export function scaffoldApp(root: string, appName: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createApp(name: string): void {
|
export function createApp(name: string, options: readonly string[] = []): void {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
console.error("Usage: wrnexus create <app-name>");
|
console.error("Usage: wrnexus create <app-name>");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
scaffoldApp(resolve(process.cwd(), name), name);
|
const root = resolve(process.cwd(), name);
|
||||||
|
scaffoldApp(root, name);
|
||||||
|
const preset = options.find((value) => value.startsWith("--preset="))?.split("=")[1];
|
||||||
|
const requested =
|
||||||
|
options
|
||||||
|
.find((value) => value.startsWith("--features="))
|
||||||
|
?.split("=")[1]
|
||||||
|
?.split(",") ?? [];
|
||||||
|
const features = new Set([
|
||||||
|
...(preset === "saas" ? ["queue", "mail", "seed", "testing"] : []),
|
||||||
|
...(preset === "email" ? ["queue", "mail", "testing"] : []),
|
||||||
|
...requested,
|
||||||
|
]);
|
||||||
|
for (const feature of features) runAdd(root, feature);
|
||||||
|
|
||||||
console.log(`✓ Created ${name}`);
|
console.log(`✓ Created ${name}`);
|
||||||
console.log(`\nNext steps:`);
|
console.log(`\nNext steps:`);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
* (`databases.<name>`), with files under `app/db/<name>/`.
|
* (`databases.<name>`), with files under `app/db/<name>/`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
||||||
import { basename, join, resolve } from "node:path";
|
import { basename, join, resolve } from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
import {
|
import {
|
||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
type PackageMigrationDefinition,
|
type PackageMigrationDefinition,
|
||||||
} from "@wrnexus/plugin";
|
} from "@wrnexus/plugin";
|
||||||
import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
|
import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
|
||||||
|
import { writeGeneratedFile } from "./write-generated.ts";
|
||||||
import {
|
import {
|
||||||
generateQueriesFile,
|
generateQueriesFile,
|
||||||
analyzeMigrations,
|
analyzeMigrations,
|
||||||
@@ -156,7 +157,7 @@ export async function regenerateQueries(
|
|||||||
.flatMap((f) => parseQueries(readFileSync(join(queriesDir, f), "utf8")));
|
.flatMap((f) => parseQueries(readFileSync(join(queriesDir, f), "utf8")));
|
||||||
const refs = await loadModelRefs(dbBase);
|
const refs = await loadModelRefs(dbBase);
|
||||||
const code = generateQueriesFile(queries, refs, dialectOf(driver));
|
const code = generateQueriesFile(queries, refs, dialectOf(driver));
|
||||||
writeFileSync(join(dbBase, "queries.gen.ts"), code, "utf8");
|
writeGeneratedFile(join(dbBase, "queries.gen.ts"), code);
|
||||||
return queries.length;
|
return queries.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,46 @@ export interface GeneratedFile {
|
|||||||
content: string;
|
content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function scaffoldResource(name: string): GeneratedFile[] {
|
||||||
|
const clean = name.replace(/[^A-Za-z0-9_-]/g, "").toLowerCase();
|
||||||
|
if (!clean) throw new TypeError("resource name is required");
|
||||||
|
const singular = clean.replace(/s$/, "");
|
||||||
|
const pascal = toPascalCase(singular);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
path: `schemas/${singular}.ts`,
|
||||||
|
content: `import { v } from "@wrnexus/validation";\n\nexport default v.object({\n name: v.string().min(1, "Required"),\n});\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `resources/${clean}.ts`,
|
||||||
|
content: `import { getDb } from "@wrnexus/db";\nimport { defineResource } from "@wrnexus/helpers";\n\nexport default defineResource({\n name: "${clean}",\n db: getDb,\n table: "${clean}",\n owner: "owner_id",\n fields: ["name"],\n permissions: { read: "${singular}:read", write: "${singular}:write" },\n});\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `api/${clean}.ts`,
|
||||||
|
content: `import resource from "../resources/${clean}.ts";\nexport const GET = resource.list;\nexport const POST = resource.create;\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `api/${clean}/[id].ts`,
|
||||||
|
content: `import resource from "../../resources/${clean}.ts";\nexport const GET = resource.get;\nexport const PUT = resource.update;\nexport const DELETE = resource.remove;\n`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `pages/${clean}.wrn`,
|
||||||
|
content: `page ${pascal}List {\n view {\n <h1>${pascal}</h1>\n <p>Generated resource page. Connect it to /api/${clean}.</p>\n }\n}\n`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function runGenerateResource(appRoot: string, name: string | undefined): void {
|
||||||
|
if (!name) throw new TypeError("Usage: wrnexus generate resource <name>");
|
||||||
|
for (const file of scaffoldResource(name)) {
|
||||||
|
const target = join(resolve(appRoot), "app", file.path);
|
||||||
|
if (existsSync(target)) continue;
|
||||||
|
mkdirSync(dirname(target), { recursive: true });
|
||||||
|
writeFileSync(target, file.content, "utf8");
|
||||||
|
console.log(`✓ Generated app/${file.path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toPascalCase(name: string): string {
|
function toPascalCase(name: string): string {
|
||||||
return name
|
return name
|
||||||
.split(/[^A-Za-z0-9]+/)
|
.split(/[^A-Za-z0-9]+/)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ Usage:
|
|||||||
wrnexus generate types [app-dir] Generate application-wide route/component/key types
|
wrnexus generate types [app-dir] Generate application-wide route/component/key types
|
||||||
wrnexus routes [app-dir] Generate typed named routes
|
wrnexus routes [app-dir] Generate typed named routes
|
||||||
wrnexus typecheck [app-dir] Generate types and check TypeScript plus every .wrn file
|
wrnexus typecheck [app-dir] Generate types and check TypeScript plus every .wrn file
|
||||||
|
wrnexus check [app-dir] Build/generate, typecheck, lint, test, and format-check
|
||||||
wrnexus mobile add <package...> Install Capacitor or Expo native packages
|
wrnexus mobile add <package...> Install Capacitor or Expo native packages
|
||||||
wrnexus mobile compile Compile .wrn pages into native Expo routes
|
wrnexus mobile compile Compile .wrn pages into native Expo routes
|
||||||
wrnexus native list List cross-platform native capabilities
|
wrnexus native list List cross-platform native capabilities
|
||||||
@@ -112,6 +113,11 @@ Update options: --dry-run previews changes; --no-verify skips post-update check/
|
|||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
const [command, ...rest] = process.argv.slice(2);
|
const [command, ...rest] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (command === "--version" || command === "-v" || command === "version") {
|
||||||
|
console.log(currentCliVersion());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
notifyIfUpdateAvailable(currentCliVersion(), command);
|
notifyIfUpdateAvailable(currentCliVersion(), command);
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
@@ -166,7 +172,7 @@ async function main(): Promise<void> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "create":
|
case "create":
|
||||||
createApp(rest[0] ?? "");
|
createApp(rest.find((value) => !value.startsWith("--")) ?? "", rest);
|
||||||
break;
|
break;
|
||||||
case "workspace": {
|
case "workspace": {
|
||||||
const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts");
|
const { addWorkspaceApp, createWorkspace } = await import("./workspace.ts");
|
||||||
@@ -191,6 +197,11 @@ async function main(): Promise<void> {
|
|||||||
}
|
}
|
||||||
case "generate":
|
case "generate":
|
||||||
case "g": {
|
case "g": {
|
||||||
|
if (rest[0] === "resource") {
|
||||||
|
const { runGenerateResource } = await import("./generate.ts");
|
||||||
|
runGenerateResource(".", rest[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (rest[0] === "routes") {
|
if (rest[0] === "routes") {
|
||||||
const { regenerateRoutes } = await import("./routes.ts");
|
const { regenerateRoutes } = await import("./routes.ts");
|
||||||
const n = regenerateRoutes(join(resolve(rest[1] ?? "."), "app"));
|
const n = regenerateRoutes(join(resolve(rest[1] ?? "."), "app"));
|
||||||
@@ -239,6 +250,14 @@ async function main(): Promise<void> {
|
|||||||
else console.log("✓ Application types are valid");
|
else console.log("✓ Application types are valid");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "check": {
|
||||||
|
const appRoot = rest.find((arg) => !arg.startsWith("--")) ?? ".";
|
||||||
|
bootstrapProfile(appRoot, "production", rest);
|
||||||
|
const { runCheck } = await import("./check.ts");
|
||||||
|
await runCheck(appRoot);
|
||||||
|
console.log("✓ Application check passed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "eject": {
|
case "eject": {
|
||||||
const { runEject } = await import("./eject.ts");
|
const { runEject } = await import("./eject.ts");
|
||||||
const args = rest.filter((a) => !a.startsWith("--"));
|
const args = rest.filter((a) => !a.startsWith("--"));
|
||||||
@@ -298,6 +317,21 @@ async function main(): Promise<void> {
|
|||||||
if (!result.ok) process.exitCode = 1;
|
if (!result.ok) process.exitCode = 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "queue": {
|
||||||
|
const values = rest.filter((value) => !value.startsWith("--"));
|
||||||
|
const { runQueueCommand } = await import("./queue-command.ts");
|
||||||
|
await runQueueCommand(values[2] ?? ".", values[0] ?? "status", [
|
||||||
|
values[1] ?? "",
|
||||||
|
...rest.filter((value) => value.startsWith("--")),
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "add": {
|
||||||
|
const values = rest.filter((value) => !value.startsWith("--"));
|
||||||
|
const { runAdd } = await import("./add.ts");
|
||||||
|
runAdd(values[2] ?? ".", values[0], values[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "security": {
|
case "security": {
|
||||||
const values = rest.filter((value) => !value.startsWith("--"));
|
const values = rest.filter((value) => !value.startsWith("--"));
|
||||||
const { runSecurityCommand } = await import("./security-command.ts");
|
const { runSecurityCommand } = await import("./security-command.ts");
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
|
import { loadAppConfig } from "@wrnexus/styles";
|
||||||
|
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||||
|
import { installSqliteQueueSchema } from "@wrnexus/queue";
|
||||||
|
|
||||||
|
export async function runQueueCommand(root: string, command: string, args: readonly string[]) {
|
||||||
|
const appRoot = resolve(root);
|
||||||
|
const config = await loadAppConfig(appRoot, process.env.WRNEXUS_PROFILE);
|
||||||
|
const selected = config.queue?.databaseName ?? "default";
|
||||||
|
const dbConfig = selected === "default" ? config.db : config.databases?.[selected];
|
||||||
|
const effective =
|
||||||
|
config.queue?.storage === "database"
|
||||||
|
? dbConfig
|
||||||
|
: { driver: "sqlite" as const, url: "file:./.wrnexus/queue.sqlite" };
|
||||||
|
if (!effective || effective.driver === "mongo")
|
||||||
|
throw new Error(`Queue database '${selected}' is not configured as SQL.`);
|
||||||
|
const db = connectFromConfig(effective, appRoot);
|
||||||
|
if (db.driver.dialect !== "sqlite")
|
||||||
|
throw new Error("Queue CLI currently requires SQLite configured storage.");
|
||||||
|
const table = config.queue?.table ?? "wrnexus_jobs";
|
||||||
|
await installSqliteQueueSchema(db, table);
|
||||||
|
try {
|
||||||
|
const id = args.find((value) => !value.startsWith("--"));
|
||||||
|
if (command === "list") {
|
||||||
|
console.table(
|
||||||
|
await db.all(
|
||||||
|
`SELECT id,name,run_at,priority,lease_owner,lease_until FROM ${table} ORDER BY run_at`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "status") {
|
||||||
|
const pending = await db.one<{ total: number }>(`SELECT COUNT(*) AS total FROM ${table}`);
|
||||||
|
const history = await db.one<{ total: number }>(
|
||||||
|
`SELECT COUNT(*) AS total FROM ${table}_history`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{ pending: Number(pending?.total ?? 0), history: Number(history?.total ?? 0) },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!id && command !== "clear") throw new Error(`wrnexus queue ${command} requires a job id`);
|
||||||
|
if (command === "inspect") {
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(await db.one(`SELECT * FROM ${table} WHERE id = ?`, [id]), null, 2),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "cancel") {
|
||||||
|
await db.exec(`DELETE FROM ${table} WHERE id = ?`, [id]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "retry") {
|
||||||
|
const row = await db.one<{ payload: string }>(
|
||||||
|
`SELECT payload FROM ${table}_history WHERE id = ? AND state = 'failed'`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
if (!row) throw new Error(`Failed job '${id}' was not found.`);
|
||||||
|
const job = JSON.parse(row.payload) as {
|
||||||
|
name: string;
|
||||||
|
runAt: number;
|
||||||
|
priority: number;
|
||||||
|
idempotencyKey?: string;
|
||||||
|
};
|
||||||
|
job.runAt = Date.now();
|
||||||
|
await db.exec(
|
||||||
|
`INSERT INTO ${table} (id,name,payload,run_at,priority,idempotency_key) VALUES (?,?,?,?,?,?)`,
|
||||||
|
[id, job.name, JSON.stringify(job), job.runAt, job.priority, job.idempotencyKey ?? null],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (command === "clear" && args.includes("--completed")) {
|
||||||
|
await db.exec(`DELETE FROM ${table}_history WHERE state = 'completed'`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
"Use queue list | status | inspect <id> | retry <id> | cancel <id> | clear --completed",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||||
|
import { dirname, extname, join, relative, resolve } from "node:path";
|
||||||
|
|
||||||
|
export interface ReachabilityIssue {
|
||||||
|
code: "WRN-REACH-MIDDLEWARE" | "WRN-REACH-QUEUE" | "WRN-REACH-WORKER";
|
||||||
|
file: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceFiles(directory: string): string[] {
|
||||||
|
if (!existsSync(directory)) return [];
|
||||||
|
const output: string[] = [];
|
||||||
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||||
|
const path = join(directory, entry.name);
|
||||||
|
if (entry.isDirectory()) output.push(...sourceFiles(path));
|
||||||
|
else if ([".ts", ".js", ".mts", ".mjs"].includes(extname(entry.name))) output.push(path);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Static registration audit for convention-discovered application modules. */
|
||||||
|
export function checkReachability(appRoot: string): ReachabilityIssue[] {
|
||||||
|
const root = resolve(appRoot);
|
||||||
|
const app = join(root, "app");
|
||||||
|
const all = sourceFiles(app).map((file) => ({ file, source: readFileSync(file, "utf8") }));
|
||||||
|
const issues: ReachabilityIssue[] = [];
|
||||||
|
|
||||||
|
for (const entry of all) {
|
||||||
|
const path = relative(root, entry.file).replace(/\\/g, "/");
|
||||||
|
if (path.startsWith("app/middleware/") && !/\bexport\s+default\b/.test(entry.source)) {
|
||||||
|
issues.push({
|
||||||
|
code: "WRN-REACH-MIDDLEWARE",
|
||||||
|
file: path,
|
||||||
|
message: "Middleware discovery requires a default export.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
path.startsWith("app/queues/") &&
|
||||||
|
/\bdefineQueue\s*\(/.test(entry.source) &&
|
||||||
|
!/\bexport\s+default\b/.test(entry.source)
|
||||||
|
) {
|
||||||
|
issues.push({
|
||||||
|
code: "WRN-REACH-QUEUE",
|
||||||
|
file: path,
|
||||||
|
message: "Queue discovery requires the defined queue to be exported as default.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
/\bdefineWorker\s*(?:<[^>]+>)?\s*\(/.test(entry.source) &&
|
||||||
|
!/\brunWorker\s*\(/.test(entry.source)
|
||||||
|
) {
|
||||||
|
const stem = entry.file.replace(/\.[^.]+$/, "");
|
||||||
|
const imported = all.some(
|
||||||
|
(candidate) =>
|
||||||
|
candidate.file !== entry.file &&
|
||||||
|
candidate.source
|
||||||
|
.replace(/\\/g, "/")
|
||||||
|
.includes(relative(dirname(candidate.file), stem).replace(/\\/g, "/")),
|
||||||
|
);
|
||||||
|
if (!imported) {
|
||||||
|
issues.push({
|
||||||
|
code: "WRN-REACH-WORKER",
|
||||||
|
file: path,
|
||||||
|
message: "Worker is defined but no application module imports it or calls runWorker().",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assertReachability(appRoot: string): void {
|
||||||
|
const issues = checkReachability(appRoot);
|
||||||
|
if (!issues.length) return;
|
||||||
|
throw new Error(
|
||||||
|
`WRN-REACHABILITY: unreachable application exports\n${issues
|
||||||
|
.map((issue) => `- ${issue.code} ${issue.file}: ${issue.message}`)
|
||||||
|
.join("\n")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
* startup; also exposed via `wrnexus generate routes`.
|
* startup; also exposed via `wrnexus generate routes`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { writeFileSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { buildRouter, generateRoutesFile } from "@wrnexus/router";
|
import { buildRouter, generateRoutesFile } from "@wrnexus/router";
|
||||||
|
import { writeGeneratedFile } from "./write-generated.ts";
|
||||||
|
|
||||||
/** Regenerate `app/routes.gen.ts`. Returns the number of page routes. */
|
/** Regenerate `app/routes.gen.ts`. Returns the number of page routes. */
|
||||||
export function regenerateRoutes(appDir: string): number {
|
export function regenerateRoutes(appDir: string): number {
|
||||||
const router = buildRouter(appDir);
|
const router = buildRouter(appDir);
|
||||||
const code = generateRoutesFile(router.pages);
|
const code = generateRoutesFile(router.pages);
|
||||||
writeFileSync(join(appDir, "routes.gen.ts"), code, "utf8");
|
writeGeneratedFile(join(appDir, "routes.gen.ts"), code);
|
||||||
return router.pages.length;
|
return router.pages.length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { registerDb, setDb } from "@wrnexus/db";
|
||||||
|
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||||
|
import { loadAppConfig, loadEnv } from "@wrnexus/styles";
|
||||||
|
|
||||||
|
const root = process.env.WRNEXUS_TEST_ROOT;
|
||||||
|
if (root) {
|
||||||
|
const profile = process.env.WRNEXUS_PROFILE || "test";
|
||||||
|
loadEnv(root, profile);
|
||||||
|
const config = await loadAppConfig(root, profile);
|
||||||
|
if (config.db) setDb(connectFromConfig(config.db, root));
|
||||||
|
for (const [name, database] of Object.entries(config.databases ?? {})) {
|
||||||
|
registerDb(name, connectFromConfig(database, root));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ export interface TestCommandPlan {
|
|||||||
level?: TestLevel;
|
level?: TestLevel;
|
||||||
files: string[];
|
files: string[];
|
||||||
setup?: { command: string; args: string[] };
|
setup?: { command: string; args: string[] };
|
||||||
|
env?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shardFiles(files: string[], value?: string): string[] {
|
function shardFiles(files: string[], value?: string): string[] {
|
||||||
@@ -69,6 +70,11 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
|
|||||||
const root = resolve(appRoot);
|
const root = resolve(appRoot);
|
||||||
const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
|
const level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
|
||||||
const watch = args.includes("--watch");
|
const watch = args.includes("--watch");
|
||||||
|
const profile = args.find((value) => value.startsWith("--profile="))?.slice(10) || "test";
|
||||||
|
const setupSource = join(import.meta.dir, "test-setup.ts");
|
||||||
|
const setupModule = existsSync(setupSource)
|
||||||
|
? setupSource
|
||||||
|
: join(import.meta.dir, "test-setup.js");
|
||||||
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
|
const shard = args.find((value) => value.startsWith("--shard="))?.slice(8);
|
||||||
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
|
const browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
|
||||||
.split(",")
|
.split(",")
|
||||||
@@ -120,10 +126,18 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
|
|||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
command: process.execPath,
|
command: process.execPath,
|
||||||
args: ["test", ...(watch ? ["--watch"] : []), ...(pattern ? files : []), ...passthrough],
|
args: [
|
||||||
|
"test",
|
||||||
|
"--preload",
|
||||||
|
setupModule,
|
||||||
|
...(watch ? ["--watch"] : []),
|
||||||
|
...(pattern ? files : []),
|
||||||
|
...passthrough,
|
||||||
|
],
|
||||||
cwd: root,
|
cwd: root,
|
||||||
level,
|
level,
|
||||||
files,
|
files,
|
||||||
|
env: { WRNEXUS_TEST_ROOT: root, WRNEXUS_PROFILE: profile },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +150,12 @@ export function runTests(appRoot: string, args: string[]): ChildProcess | null {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const launch = () => spawn(plan.command, plan.args, { stdio: "inherit", cwd: plan.cwd });
|
const launch = () =>
|
||||||
|
spawn(plan.command, plan.args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: plan.cwd,
|
||||||
|
env: { ...process.env, ...plan.env },
|
||||||
|
});
|
||||||
if (plan.setup) {
|
if (plan.setup) {
|
||||||
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
|
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
|
||||||
setup.on("exit", (code, signal) => {
|
setup.on("exit", (code, signal) => {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { generate, generateTargets } from "@wrnexus/compiler";
|
|||||||
import { regenerateRoutes } from "./routes.ts";
|
import { regenerateRoutes } from "./routes.ts";
|
||||||
import { loadAppConfig } from "@wrnexus/styles";
|
import { loadAppConfig } from "@wrnexus/styles";
|
||||||
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
|
import { createPluginRunner, discoverPlugins, type PluginContributions } from "@wrnexus/plugin";
|
||||||
|
import { writeGeneratedFile } from "./write-generated.ts";
|
||||||
|
|
||||||
function files(root: string, predicate: (path: string) => boolean): string[] {
|
function files(root: string, predicate: (path: string) => boolean): string[] {
|
||||||
if (!existsSync(root)) return [];
|
if (!existsSync(root)) return [];
|
||||||
@@ -309,7 +310,7 @@ declare namespace WRNexusGenerated {
|
|||||||
`;
|
`;
|
||||||
mkdirSync(typeDir, { recursive: true });
|
mkdirSync(typeDir, { recursive: true });
|
||||||
const output = join(typeDir, "wrnexus.generated.d.ts");
|
const output = join(typeDir, "wrnexus.generated.d.ts");
|
||||||
writeFileSync(output, code, "utf8");
|
writeGeneratedFile(output, code);
|
||||||
// `.d.ts` contents are exempt from checking under `skipLibCheck: true` (set in the
|
// `.d.ts` contents are exempt from checking under `skipLibCheck: true` (set in the
|
||||||
// repo/app tsconfig), so the per-block assertions are written into a real `.ts` file
|
// repo/app tsconfig), so the per-block assertions are written into a real `.ts` file
|
||||||
// instead — only genuine `.ts`/`.tsx` sources are compiled and checked.
|
// instead — only genuine `.ts`/`.tsx` sources are compiled and checked.
|
||||||
@@ -321,7 +322,7 @@ declare namespace WRNexusGenerated {
|
|||||||
${apiBlockAssertions(pageAsts, apiContracts, app)}
|
${apiBlockAssertions(pageAsts, apiContracts, app)}
|
||||||
export {};
|
export {};
|
||||||
`;
|
`;
|
||||||
writeFileSync(join(typeDir, "wrnexus.generated.api-checks.ts"), apiChecksCode, "utf8");
|
writeGeneratedFile(join(typeDir, "wrnexus.generated.api-checks.ts"), apiChecksCode);
|
||||||
writePluginArtifacts(root, pluginContributions);
|
writePluginArtifacts(root, pluginContributions);
|
||||||
return {
|
return {
|
||||||
file: relative(root, output).replace(/\\/g, "/"),
|
file: relative(root, output).replace(/\\/g, "/"),
|
||||||
|
|||||||
@@ -63,14 +63,16 @@ const execFileAsync = promisify(execFile);
|
|||||||
|
|
||||||
/** Resolve every installed WRNexus dependency independently and in parallel. */
|
/** Resolve every installed WRNexus dependency independently and in parallel. */
|
||||||
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> {
|
export async function latestPackageVersions(names: string[]): Promise<Map<string, string>> {
|
||||||
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
||||||
const unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
|
const unique = [...new Set(names.filter((name) => name.startsWith("@wrnexus/")))];
|
||||||
const entries = await Promise.all(
|
const entries = await Promise.all(
|
||||||
unique.map(async (name) => {
|
unique.map(async (name) => {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], {
|
const command = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "npm";
|
||||||
encoding: "utf8",
|
const args =
|
||||||
});
|
process.platform === "win32"
|
||||||
|
? ["/d", "/s", "/c", "npm", "view", name, "version", "--json"]
|
||||||
|
: ["view", name, "version", "--json"];
|
||||||
|
const { stdout } = await execFileAsync(command, args, { encoding: "utf8" });
|
||||||
const parsed = JSON.parse(stdout.trim()) as string | string[];
|
const parsed = JSON.parse(stdout.trim()) as string | string[];
|
||||||
const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
const version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
||||||
return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)
|
return typeof version === "string" && /^\d+\.\d+\.\d+/.test(version)
|
||||||
@@ -1274,6 +1276,13 @@ export async function runUpdate(dir: string, args: string[]): Promise<void> {
|
|||||||
? new Map<string, string>()
|
? new Map<string, string>()
|
||||||
: await latestPackageVersions(dependencyNames);
|
: await latestPackageVersions(dependencyNames);
|
||||||
|
|
||||||
|
if (!versionArg && dependencyNames.length > 0 && packageVersions.size === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to resolve any of ${new Set(dependencyNames).size} installed WRNexus package ` +
|
||||||
|
`version(s) from npm. Check the @wrnexus registry and authentication before retrying.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`\n ⚡ wrnexus update → migrations ${target}${dryRun ? " (dry run)" : ""}`);
|
console.log(`\n ⚡ wrnexus update → migrations ${target}${dryRun ? " (dry run)" : ""}`);
|
||||||
if (!versionArg) {
|
if (!versionArg) {
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
/** Write generated UTF-8 output only when bytes changed, preserving mtimes on no-op builds. */
|
||||||
|
export function writeGeneratedFile(path: string, content: string): boolean {
|
||||||
|
const normalized = content.replace(/\r\n/g, "\n");
|
||||||
|
if (existsSync(path) && readFileSync(path, "utf8").replace(/\r\n/g, "\n") === normalized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
writeFileSync(path, normalized, "utf8");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -51,9 +51,7 @@ test("scaffoldApp includes production build and start scripts", () => {
|
|||||||
expect(pkg.scripts.production).toBe("bun run build && bun run start");
|
expect(pkg.scripts.production).toBe("bun run build && bun run start");
|
||||||
expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
|
expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
|
||||||
expect(pkg.scripts.test).toBe("wrnexus test .");
|
expect(pkg.scripts.test).toBe("wrnexus test .");
|
||||||
expect(pkg.scripts.check).toBe(
|
expect(pkg.scripts.check).toBe("wrnexus check .");
|
||||||
"bun run typecheck && bun run lint && bun run test && bun run format:check",
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(parent, { recursive: true, force: true });
|
rmSync(parent, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { test, expect } from "bun:test";
|
import { test, expect } from "bun:test";
|
||||||
import { scaffold } from "../src/generate.ts";
|
import { scaffold, scaffoldResource } from "../src/generate.ts";
|
||||||
|
|
||||||
test("scaffold page: kebab file, PascalCase page name", () => {
|
test("scaffold page: kebab file, PascalCase page name", () => {
|
||||||
const f = scaffold("page", "about-us");
|
const f = scaffold("page", "about-us");
|
||||||
@@ -32,3 +32,15 @@ test("scaffold strips extensions from the given name", () => {
|
|||||||
expect(scaffold("page", "home.wrn").path).toBe("pages/home.wrn");
|
expect(scaffold("page", "home.wrn").path).toBe("pages/home.wrn");
|
||||||
expect(scaffold("api", "ping.ts").path).toBe("api/ping.ts");
|
expect(scaffold("api", "ping.ts").path).toBe("api/ping.ts");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("resource scaffold emits schema, handlers, routes and a page", () => {
|
||||||
|
const files = scaffoldResource("templates");
|
||||||
|
expect(files.map((file) => file.path)).toEqual([
|
||||||
|
"schemas/template.ts",
|
||||||
|
"resources/templates.ts",
|
||||||
|
"api/templates.ts",
|
||||||
|
"api/templates/[id].ts",
|
||||||
|
"pages/templates.wrn",
|
||||||
|
]);
|
||||||
|
expect(files[1]?.content).toContain("defineResource");
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { checkReachability } from "../src/reachability.ts";
|
||||||
|
|
||||||
|
test("reports convention modules and workers that cannot be reached", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrn-reach-"));
|
||||||
|
mkdirSync(join(root, "app", "middleware"), { recursive: true });
|
||||||
|
mkdirSync(join(root, "app", "queues"), { recursive: true });
|
||||||
|
writeFileSync(join(root, "app", "middleware", "audit.ts"), "export const audit = () => {};\n");
|
||||||
|
writeFileSync(
|
||||||
|
join(root, "app", "queues", "mail.ts"),
|
||||||
|
"export const mail = defineQueue({}); export const worker = defineWorker({});\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
checkReachability(root)
|
||||||
|
.map((issue) => issue.code)
|
||||||
|
.sort(),
|
||||||
|
).toEqual(["WRN-REACH-MIDDLEWARE", "WRN-REACH-QUEUE", "WRN-REACH-WORKER"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts discovered middleware and queues", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrn-reach-"));
|
||||||
|
mkdirSync(join(root, "app", "middleware"), { recursive: true });
|
||||||
|
mkdirSync(join(root, "app", "queues"), { recursive: true });
|
||||||
|
writeFileSync(join(root, "app", "middleware", "audit.ts"), "export default () => {};\n");
|
||||||
|
writeFileSync(join(root, "app", "queues", "mail.ts"), "export default defineQueue({});\n");
|
||||||
|
expect(checkReachability(root)).toEqual([]);
|
||||||
|
});
|
||||||
@@ -50,6 +50,15 @@ describe("test command planning", () => {
|
|||||||
test("keeps the unfiltered legacy command", async () => {
|
test("keeps the unfiltered legacy command", async () => {
|
||||||
const plan = createTestPlan(await fixture(), ["--watch"]);
|
const plan = createTestPlan(await fixture(), ["--watch"]);
|
||||||
expect(plan.level).toBeUndefined();
|
expect(plan.level).toBeUndefined();
|
||||||
expect(plan.args).toEqual(["test", "--watch"]);
|
expect(plan.args[0]).toBe("test");
|
||||||
|
expect(plan.args).toContain("--preload");
|
||||||
|
expect(plan.args.at(-1)).toBe("--watch");
|
||||||
|
expect(plan.env?.WRNEXUS_PROFILE).toBe("test");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("passes the selected profile to automatic database setup", async () => {
|
||||||
|
const plan = createTestPlan(await fixture(), ["--profile=ci"]);
|
||||||
|
expect(plan.env?.WRNEXUS_PROFILE).toBe("ci");
|
||||||
|
expect(plan.args).not.toContain("--profile=ci");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import { afterEach, expect, test } from "bun:test";
|
import { afterEach, expect, test } from "bun:test";
|
||||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join, resolve } from "node:path";
|
||||||
import { compareVersions, notifyIfUpdateAvailable } from "../src/update-notifier.ts";
|
import {
|
||||||
|
compareVersions,
|
||||||
|
currentCliVersion,
|
||||||
|
notifyIfUpdateAvailable,
|
||||||
|
} from "../src/update-notifier.ts";
|
||||||
|
|
||||||
const originalCache = process.env.WRNEXUS_UPDATE_CACHE;
|
const originalCache = process.env.WRNEXUS_UPDATE_CACHE;
|
||||||
|
|
||||||
@@ -35,3 +39,21 @@ test("cached update checks suggest the exact upgrade command", () => {
|
|||||||
rmSync(root, { recursive: true, force: true });
|
rmSync(root, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("--version prints the package version outside a WrNexus workspace", () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-version-"));
|
||||||
|
try {
|
||||||
|
const result = Bun.spawnSync({
|
||||||
|
cmd: [process.execPath, resolve(import.meta.dir, "../src/index.ts"), "--version"],
|
||||||
|
cwd: root,
|
||||||
|
env: { ...process.env, WRNEXUS_NO_UPDATE_CHECK: "1" },
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.stdout.toString().trim()).toBe(currentCliVersion());
|
||||||
|
expect(result.stderr.toString()).toBe("");
|
||||||
|
} finally {
|
||||||
|
rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.14",
|
"version": "0.8.22",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -138,6 +138,29 @@ function isHtmlBooleanAttribute(name: string): boolean {
|
|||||||
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
return HTML_BOOLEAN_ATTRIBUTES.has(name.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Constraint attributes that must be absent rather than empty. `pattern` is the
|
||||||
|
* dangerous one: an empty pattern compiles to a regex matching only the empty
|
||||||
|
* string, so every typed value becomes invalid and the form silently refuses to
|
||||||
|
* submit. A component declaring `pattern: string = ""` and rendering
|
||||||
|
* `pattern="{pattern}"` therefore breaks every input that did not ask for a
|
||||||
|
* pattern. The rest are inert when empty, but carry no meaning either.
|
||||||
|
*/
|
||||||
|
const OMIT_WHEN_EMPTY_ATTRIBUTES = new Set([
|
||||||
|
"pattern",
|
||||||
|
"minlength",
|
||||||
|
"maxlength",
|
||||||
|
"min",
|
||||||
|
"max",
|
||||||
|
"step",
|
||||||
|
"inputmode",
|
||||||
|
"accept",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isOmitWhenEmptyAttribute(name: string): boolean {
|
||||||
|
return OMIT_WHEN_EMPTY_ATTRIBUTES.has(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const URL_ATTRIBUTES = new Set([
|
const URL_ATTRIBUTES = new Set([
|
||||||
"href",
|
"href",
|
||||||
"src",
|
"src",
|
||||||
@@ -203,9 +226,11 @@ function renderAttr(attr: Attr): string {
|
|||||||
case "csrText":
|
case "csrText":
|
||||||
return "";
|
return "";
|
||||||
default:
|
default:
|
||||||
return attr.boolean
|
if (attr.boolean) return ` ${attr.name}`;
|
||||||
? ` ${attr.name}`
|
// An empty constraint attribute is worse than absent -- see
|
||||||
: ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
// OMIT_WHEN_EMPTY_ATTRIBUTES.
|
||||||
|
if (attr.value === "" && isOmitWhenEmptyAttribute(attr.name)) return "";
|
||||||
|
return ` ${attr.name}="${attrEscape(safeAttributeValue(attr.name, attr.value))}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +283,32 @@ function componentEventAttribute(name: string): string {
|
|||||||
return `data-wrn-out-${name}`;
|
return `data-wrn-out-${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Expand bind:value/bind:checked into a reactive prop plus assignment handler. */
|
||||||
|
function expandBindings(attrs: Attr[]): Attr[] {
|
||||||
|
const output: Attr[] = [];
|
||||||
|
for (const attr of attrs) {
|
||||||
|
if (attr.event || !attr.name.startsWith("bind:")) {
|
||||||
|
output.push(attr);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const target = attr.name.slice("bind:".length);
|
||||||
|
if (target !== "value" && target !== "checked") {
|
||||||
|
throw new Error(`Unknown .wrn binding '${attr.name}'. Use bind:value or bind:checked.`);
|
||||||
|
}
|
||||||
|
const expression = wholeAttributeExpression(attr.value) ?? attr.value.trim();
|
||||||
|
if (!/^[A-Za-z_$][\w$]*$/.test(expression)) {
|
||||||
|
throw new Error(`${attr.name} requires a writable state name, received '${expression}'.`);
|
||||||
|
}
|
||||||
|
output.push({ name: target, value: `{${expression}}`, event: false });
|
||||||
|
output.push({
|
||||||
|
name: target === "checked" ? "change" : "input",
|
||||||
|
value: `${expression} = payload.${target}`,
|
||||||
|
event: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
|
function reactiveAttrValue(raw: string, reactive: PageReactive): string | null {
|
||||||
let found = false;
|
let found = false;
|
||||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
|
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner: string) => {
|
||||||
@@ -280,6 +331,7 @@ function renderAttrs(
|
|||||||
reactive: PageReactive | null = null,
|
reactive: PageReactive | null = null,
|
||||||
dynamicExpressions?: string[],
|
dynamicExpressions?: string[],
|
||||||
): string {
|
): string {
|
||||||
|
attrs = expandBindings(attrs);
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const rendered = attrs
|
const rendered = attrs
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
@@ -423,22 +475,22 @@ function bakeLoopAttr(raw: string, typed = false): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Render one loop-body node to template-literal source (nested loops inline). */
|
/** Render one loop-body node to template-literal source (nested loops inline). */
|
||||||
function renderLoopBody(node: ViewNode): string {
|
function renderLoopBody(node: ViewNode, locals: string[] = []): string {
|
||||||
if (node.type === "text") {
|
if (node.type === "text") {
|
||||||
return bakeLoopText(node.value);
|
return bakeLoopText(node.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === "each") {
|
if (node.type === "each") {
|
||||||
return compileEachExpr(node);
|
return compileEachExpr(node, locals);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === "if") {
|
if (node.type === "if") {
|
||||||
return compileIfExpr(node);
|
return compileIfExpr(node, locals);
|
||||||
}
|
}
|
||||||
|
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
|
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const name = attr.event
|
const name = attr.event
|
||||||
@@ -455,7 +507,16 @@ function renderLoopBody(node: ViewNode): string {
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
const inner = node.children.map(renderLoopBody).join("");
|
// A handler expression is emitted as text and evaluated at event time, so any
|
||||||
|
// `{#each}` variable it names has to travel with the element. The runtime
|
||||||
|
// resolves them with closest("[data-wrn-loop-locals]"). Only elements that
|
||||||
|
// actually bind an event need it -- marking every node would bloat the HTML.
|
||||||
|
const localsAttr =
|
||||||
|
locals.length > 0 && node.attrs.some((attr) => attr.event) ? loopLocalsAttr(locals) : "";
|
||||||
|
|
||||||
|
const inner = node.children.map((child) => renderLoopBody(child, locals)).join("");
|
||||||
|
|
||||||
|
const openAttrs = attrs + localsAttr;
|
||||||
|
|
||||||
if (node.tag === "Static") return inner;
|
if (node.tag === "Static") return inner;
|
||||||
if (node.tag === "Dynamic")
|
if (node.tag === "Dynamic")
|
||||||
@@ -511,7 +572,7 @@ function renderLoopBody(node: ViewNode): string {
|
|||||||
if (island) return escLit(island);
|
if (island) return escLit(island);
|
||||||
return (
|
return (
|
||||||
escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
escLit(`<div data-component="${attrEscape(node.tag)}"`) +
|
||||||
attrs +
|
openAttrs +
|
||||||
escLit(">") +
|
escLit(">") +
|
||||||
inner +
|
inner +
|
||||||
escLit("</div>")
|
escLit("</div>")
|
||||||
@@ -519,10 +580,20 @@ function renderLoopBody(node: ViewNode): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">");
|
return escLit(`<${node.tag}`) + openAttrs + escLit(">");
|
||||||
}
|
}
|
||||||
|
|
||||||
return escLit(`<${node.tag}`) + attrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
return escLit(`<${node.tag}`) + openAttrs + escLit(">") + inner + escLit(`</${node.tag}>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the ` data-wrn-loop-locals="..."` attribute for a page-rendered loop
|
||||||
|
* body. Deliberately not passed through escLit: the `${...}` must stay live so
|
||||||
|
* the values are encoded at render time.
|
||||||
|
*/
|
||||||
|
function loopLocalsAttr(locals: string[]): string {
|
||||||
|
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
|
||||||
|
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -530,11 +601,13 @@ function renderLoopBody(node: ViewNode): string {
|
|||||||
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
|
* that iterates the (server-evaluated) list and joins the per-item body. `list` is a
|
||||||
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
|
* JS expression evaluated where `ssr` data bindings are in scope as raw named values.
|
||||||
*/
|
*/
|
||||||
function compileEachExpr(node: EachNode): string {
|
function compileEachExpr(node: EachNode, outerLocals: string[] = []): string {
|
||||||
const item = node.item;
|
const item = node.item;
|
||||||
const index = node.index ?? "__wi";
|
const index = node.index ?? "__wi";
|
||||||
const body = node.body.map(renderLoopBody).join("");
|
// A nested loop can reference the outer loop's variables too.
|
||||||
const empty = node.empty.map(renderLoopBody).join("");
|
const locals = [...outerLocals, item, index];
|
||||||
|
const body = node.body.map((child) => renderLoopBody(child, locals)).join("");
|
||||||
|
const empty = node.empty.map((child) => renderLoopBody(child, outerLocals)).join("");
|
||||||
return (
|
return (
|
||||||
"${(() => { const __wl = Array.isArray(" +
|
"${(() => { const __wl = Array.isArray(" +
|
||||||
node.list +
|
node.list +
|
||||||
@@ -557,11 +630,11 @@ function compileEachExpr(node: EachNode): string {
|
|||||||
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
|
* that renders the first truthy branch's body (or the `{:else}` body, or "" when neither).
|
||||||
* Conditions are JS expressions evaluated in the surrounding server scope.
|
* Conditions are JS expressions evaluated in the surrounding server scope.
|
||||||
*/
|
*/
|
||||||
function compileIfExpr(node: IfNode): string {
|
function compileIfExpr(node: IfNode, locals: string[] = []): string {
|
||||||
let expr = "``"; // no matching branch → empty string
|
let expr = "``"; // no matching branch → empty string
|
||||||
for (let k = node.branches.length - 1; k >= 0; k--) {
|
for (let k = node.branches.length - 1; k >= 0; k--) {
|
||||||
const b = node.branches[k]!;
|
const b = node.branches[k]!;
|
||||||
const bodySrc = "`" + b.body.map(renderLoopBody).join("") + "`";
|
const bodySrc = "`" + b.body.map((child) => renderLoopBody(child, locals)).join("") + "`";
|
||||||
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
expr = b.cond === null ? bodySrc : "(" + b.cond + ") ? " + bodySrc + " : " + expr;
|
||||||
}
|
}
|
||||||
return "${" + expr + "}";
|
return "${" + expr + "}";
|
||||||
@@ -759,9 +832,9 @@ function renderPageComponentInvocation(
|
|||||||
const island = islandMarkerFor(node);
|
const island = islandMarkerFor(node);
|
||||||
if (island) return island;
|
if (island) return island;
|
||||||
|
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
const inner = node.children
|
const inner = node.children
|
||||||
@@ -781,7 +854,7 @@ function renderNestedComponentInvocation(
|
|||||||
|
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
|
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((attr) => attr.name !== "data-component")
|
.filter((attr) => attr.name !== "data-component")
|
||||||
.map((attr) => {
|
.map((attr) => {
|
||||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||||
@@ -1619,6 +1692,18 @@ function generateInner(ast: PageAst): string {
|
|||||||
if (needsRuntimeHelpers) {
|
if (needsRuntimeHelpers) {
|
||||||
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
|
out.push(`import { buildApiRequest as __wrnexusBuildApiRequest } from "@wrnexus/core";`);
|
||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
|
// Loop bodies that bind an event carry their {#each} locals in an encoded
|
||||||
|
// attribute. Emitted only when the view actually produced one, so a page
|
||||||
|
// without handlers in a loop keeps the smaller prelude -- but it MUST be
|
||||||
|
// emitted whenever the marker is, or the render throws on an undefined
|
||||||
|
// function instead of the handler throwing on an undefined variable.
|
||||||
|
if (body.includes("__wrnexusEncodeLoopLocals(")) {
|
||||||
|
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
|
||||||
|
const json = JSON.stringify(value);
|
||||||
|
const buffer = (globalThis as { Buffer?: { from(i: string, e: string): { toString(e: string): string } } }).Buffer;
|
||||||
|
return buffer ? buffer.from(json, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(json)));
|
||||||
|
}`);
|
||||||
|
}
|
||||||
out.push(
|
out.push(
|
||||||
`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`,
|
`const __wrnexusSsrBindings: __WrnexusSsrBinding[] = ${JSON.stringify(ssrBindings, null, 2)};`,
|
||||||
);
|
);
|
||||||
@@ -1772,6 +1857,8 @@ ${declarations}
|
|||||||
${
|
${
|
||||||
visible.length
|
visible.length
|
||||||
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
? ` const __values = await Promise.all([${visible.map((entry) => `__load_${entry.name}()`).join(", ")}]);
|
||||||
|
const __response = __values.find((value) => value instanceof Response);
|
||||||
|
if (__response) return __response;
|
||||||
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
|
return { ${visible.map((entry, index) => `${JSON.stringify(entry.name)}: __values[${index}]`).join(", ")} };`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
@@ -2193,7 +2280,7 @@ function renderClientControlTemplate(nodes: ViewNode[]): string {
|
|||||||
|
|
||||||
const componentTag = isComponentTag(node.tag);
|
const componentTag = isComponentTag(node.tag);
|
||||||
let bindIndex = 0;
|
let bindIndex = 0;
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.map((attribute) => {
|
.map((attribute) => {
|
||||||
const name = attribute.event
|
const name = attribute.event
|
||||||
? componentTag
|
? componentTag
|
||||||
@@ -2235,24 +2322,27 @@ function encodeClientControl(value: unknown): string {
|
|||||||
|
|
||||||
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
|
function renderComponentIfNode(node: IfNode, ctx: CompCtx): string {
|
||||||
let expression = "``";
|
let expression = "``";
|
||||||
|
const clientBranches: string[] = [];
|
||||||
|
|
||||||
for (let index = node.branches.length - 1; index >= 0; index--) {
|
for (let index = node.branches.length - 1; index >= 0; index--) {
|
||||||
const branch = node.branches[index]!;
|
const branch = node.branches[index]!;
|
||||||
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
||||||
const bodyExpression = "`" + body + "`";
|
const bodyExpression = "`" + body + "`";
|
||||||
|
|
||||||
|
clientBranches.unshift(
|
||||||
|
`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`,
|
||||||
|
);
|
||||||
|
|
||||||
expression =
|
expression =
|
||||||
branch.cond === null
|
branch.cond === null
|
||||||
? bodyExpression
|
? bodyExpression
|
||||||
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = encodeClientControl(
|
// Render all branches on the server into an inert, encoded definition.
|
||||||
node.branches.map((branch) => ({
|
// A branch first selected in the browser therefore contains the real nested
|
||||||
cond: branch.cond,
|
// component HTML and scope metadata, not a data-component placeholder.
|
||||||
body: renderClientControlTemplate(branch.body),
|
const definition = `\${__wrnexusEncodeControl([${clientBranches.join(",")}])}`;
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
return `<template data-wrn-if="${definition}"></template>${"${" + expression + "}"}<template data-wrn-control-end></template>`;
|
||||||
}
|
}
|
||||||
@@ -2431,7 +2521,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
|||||||
(attribute) => attribute.name === "data-component",
|
(attribute) => attribute.name === "data-component",
|
||||||
);
|
);
|
||||||
|
|
||||||
const attrs = node.attrs
|
const attrs = expandBindings(node.attrs)
|
||||||
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
.filter((a) => a.name !== "class" && !a.name.startsWith("class:"))
|
||||||
.map((a) => {
|
.map((a) => {
|
||||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||||
@@ -2505,6 +2595,19 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
|||||||
isExplicitComponentMount && wholeExpression
|
isExplicitComponentMount && wholeExpression
|
||||||
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||||
: compileAttrValue(a.value, elementContext);
|
: compileAttrValue(a.value, elementContext);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A constraint attribute whose value is only known at render time (a
|
||||||
|
* component prop defaulting to "") must be decided at render time, so
|
||||||
|
* the whole attribute -- name included -- is emitted by the helper.
|
||||||
|
* Component mounts are excluded: there the value is a prop being passed
|
||||||
|
* down, not an HTML attribute being written.
|
||||||
|
*/
|
||||||
|
if (isOmitWhenEmptyAttribute(a.name) && !isExplicitComponentMount) {
|
||||||
|
if (a.value === "") return "";
|
||||||
|
return `\${__wrnOptionalAttr(${JSON.stringify(a.name)}, \`${compiledValue}\`)}`;
|
||||||
|
}
|
||||||
|
|
||||||
const rendered = ` ${a.name}="${compiledValue}"`;
|
const rendered = ` ${a.name}="${compiledValue}"`;
|
||||||
|
|
||||||
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
const referencesState = exprRefsComponentReactiveValue(a.value, ctx);
|
||||||
@@ -2880,6 +2983,10 @@ function __wrnHtml(v: unknown): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wrnOptionalAttr(name: string, value: string): string {
|
||||||
|
return value === "" ? "" : " " + name + '="' + value + '"';
|
||||||
|
}
|
||||||
|
|
||||||
function __wrnAttr(v: unknown): string {
|
function __wrnAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
@@ -2961,6 +3068,9 @@ function __wrnRaw(v: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
|
out.push(`function __wrnexusEncodeControl(value: unknown): string {
|
||||||
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
|
}`);
|
||||||
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
@@ -3046,6 +3156,10 @@ function __wrnHtml(v: unknown): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wrnOptionalAttr(name: string, value: string): string {
|
||||||
|
return value === "" ? "" : " " + name + '="' + value + '"';
|
||||||
|
}
|
||||||
|
|
||||||
function __wrnAttr(v: unknown): string {
|
function __wrnAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||||||
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
||||||
@@ -3058,7 +3172,11 @@ function __wrnProp(v: unknown): string {
|
|||||||
|
|
||||||
return __wrnAttr(value);
|
return __wrnAttr(value);
|
||||||
}
|
}
|
||||||
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string {
|
function renderPageComponentAttr(
|
||||||
|
attr: Attr,
|
||||||
|
dynamicExpressions: string[],
|
||||||
|
_reactive: PageReactive | null,
|
||||||
|
): string {
|
||||||
if (attr.event) {
|
if (attr.event) {
|
||||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||||
}
|
}
|
||||||
@@ -3076,6 +3194,12 @@ function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): stri
|
|||||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||||
|
|
||||||
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
|
||||||
|
// The mount disappears during SSR. Preserve every dynamic component prop
|
||||||
|
// as a parent-owned binding; server-only expressions simply fail closed in
|
||||||
|
// the browser, while page state can continue driving the mounted child.
|
||||||
|
const binding = ` data-wrn-prop-bind-${dynamicExpressions.length - 1}="${attrEscape(
|
||||||
|
JSON.stringify([attr.name, attr.value]),
|
||||||
|
)}"`;
|
||||||
|
|
||||||
return ` ${attr.name}="${marker}"`;
|
return ` ${attr.name}="${marker}"${binding}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Attr, PageAst, ViewNode } from "@wrnexus/syntax";
|
import { extractInterpolations, type Attr, type PageAst, type ViewNode } from "@wrnexus/syntax";
|
||||||
|
|
||||||
export class NativeCompileError extends Error {
|
export class NativeCompileError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
@@ -68,11 +68,11 @@ function expression(value: string): string | null {
|
|||||||
function textJsx(value: string): string {
|
function textJsx(value: string): string {
|
||||||
const pieces: string[] = [];
|
const pieces: string[] = [];
|
||||||
let last = 0;
|
let last = 0;
|
||||||
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
for (const segment of extractInterpolations(value)) {
|
||||||
if (match.index! > last) pieces.push(value.slice(last, match.index));
|
if (segment.start > last) pieces.push(value.slice(last, segment.start));
|
||||||
const expr = match[1]!.trim();
|
const expr = segment.expression;
|
||||||
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
pieces.push(expr.startsWith("t:") ? `{${JSON.stringify(expr.slice(2).trim())}}` : `{${expr}}`);
|
||||||
last = match.index! + match[0].length;
|
last = segment.end;
|
||||||
}
|
}
|
||||||
pieces.push(value.slice(last));
|
pieces.push(value.slice(last));
|
||||||
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
||||||
|
|||||||
@@ -133,6 +133,10 @@ function __wrnHtml(v: unknown): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wrnOptionalAttr(name: string, value: string): string {
|
||||||
|
return value === "" ? "" : " " + name + '="' + value + '"';
|
||||||
|
}
|
||||||
|
|
||||||
function __wrnAttr(v: unknown): string {
|
function __wrnAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
@@ -207,6 +211,10 @@ function __wrnRaw(v: unknown): string {
|
|||||||
return String(v == null ? "" : v);
|
return String(v == null ? "" : v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function __wrnexusEncodeControl(value: unknown): string {
|
||||||
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
|
}
|
||||||
|
|
||||||
function __wrnexusSerializeScopeValue(value: unknown): string {
|
function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ test("named loads support memoized dependencies and deferred execution", () => {
|
|||||||
expect(code).toContain('return { "audit": __values[0] }');
|
expect(code).toContain('return { "audit": __values[0] }');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("named server loads propagate redirect responses instead of wrapping them as data", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page Private {
|
||||||
|
load server gate { return Response.redirect("/login", 303) }
|
||||||
|
view { <p>Private</p> }
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).toContain("__values.find((value) => value instanceof Response)");
|
||||||
|
expect(code).toContain("if (__response) return __response");
|
||||||
|
});
|
||||||
|
|
||||||
test("load dependency cycles and cross-phase server dependencies fail compilation", () => {
|
test("load dependency cycles and cross-phase server dependencies fail compilation", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
parse(
|
parse(
|
||||||
|
|||||||
@@ -1070,6 +1070,25 @@ component Banner {
|
|||||||
expect(output).toContain("visible");
|
expect(output).toContain("visible");
|
||||||
expect(output).toContain("Visible");
|
expect(output).toContain("Visible");
|
||||||
expect(output).toContain("Hidden");
|
expect(output).toContain("Hidden");
|
||||||
|
expect(output).toContain("__wrnexusEncodeControl");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bind directives compile to reactive values and assignment handlers", () => {
|
||||||
|
const output = generate(
|
||||||
|
parse(`page FormPage {
|
||||||
|
state name = ""
|
||||||
|
state active = false
|
||||||
|
view {
|
||||||
|
<Input bind:value={name} />
|
||||||
|
<input bind:value={name} />
|
||||||
|
<Switch bind:checked={active} />
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(output).not.toContain("bind:value");
|
||||||
|
expect(output).not.toContain("bind:checked");
|
||||||
|
expect(output).toContain("name = payload.value");
|
||||||
|
expect(output).toContain("active = payload.checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("if and each blocks emit browser control metadata while preserving SSR", () => {
|
test("if and each blocks emit browser control metadata while preserving SSR", () => {
|
||||||
@@ -1132,6 +1151,16 @@ page Home {
|
|||||||
|
|
||||||
expect(output).toContain("__wrnexusPropAttr([");
|
expect(output).toContain("__wrnexusPropAttr([");
|
||||||
});
|
});
|
||||||
|
test("page state passed to a component retains a parent-owned prop binding", () => {
|
||||||
|
const output = generate(
|
||||||
|
parse(`page Home {
|
||||||
|
state count = 1
|
||||||
|
view { <Child value={count} /> }
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(output).toContain("data-wrn-prop-bind-");
|
||||||
|
expect(output).toContain("{count}");
|
||||||
|
});
|
||||||
test("nested component props retain parent-owned reactive bindings", () => {
|
test("nested component props retain parent-owned reactive bindings", () => {
|
||||||
const output = generate(
|
const output = generate(
|
||||||
parse(`component Parent {
|
parse(`component Parent {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { compile } from "../src/index.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `pattern=""` is not inert. An empty pattern compiles to a regex that matches
|
||||||
|
* only the empty string, so EVERY typed value becomes invalid and the form
|
||||||
|
* silently refuses to submit -- no error, no request.
|
||||||
|
*
|
||||||
|
* `@wrnexus/ui`'s input declares `pattern: string = ""` and renders
|
||||||
|
* `pattern="{pattern}"`, so every input that did not opt into a pattern shipped
|
||||||
|
* one that could never match. This broke sign-up in a real app.
|
||||||
|
*
|
||||||
|
* Attributes reach the output through two different emitters, and both matter:
|
||||||
|
* a component's interpolated attribute is baked at render time via
|
||||||
|
* `__wrnAttr`, while a page's static element is serialized by `renderAttr`.
|
||||||
|
*/
|
||||||
|
const uiInput = join(import.meta.dir, "..", "..", "ui", "components", "input.wrn");
|
||||||
|
|
||||||
|
function render(view: string): string {
|
||||||
|
return compile(`page P {\n view { ${view} }\n}\n`, "P.wrn").code;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the real ui input never emits a bare pattern attribute", () => {
|
||||||
|
const code = compile(readFileSync(uiInput, "utf8"), uiInput).code;
|
||||||
|
|
||||||
|
// The broken shape: the attribute is always present, empty or not.
|
||||||
|
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
|
||||||
|
// The fixed shape: the attribute itself is decided at render time.
|
||||||
|
expect(code).toContain("__wrnOptionalAttr");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an interpolated constraint attribute on a component is emitted through the helper", () => {
|
||||||
|
const source = `component Field {\n props { pattern: string = "" }\n view { <input type="text" pattern="{pattern}" /> }\n}\n`;
|
||||||
|
const code = compile(source, "Field.wrn").code;
|
||||||
|
|
||||||
|
expect(code).toContain("__wrnOptionalAttr");
|
||||||
|
expect(code).not.toContain('pattern="${__wrnAttr(pattern)}"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a static empty constraint attribute on a page element is dropped", () => {
|
||||||
|
const code = render(`<input type="email" pattern="" minlength="" />`);
|
||||||
|
|
||||||
|
expect(code).not.toContain('pattern=""');
|
||||||
|
expect(code).not.toContain('minlength=""');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a populated constraint attribute survives on both paths", () => {
|
||||||
|
expect(render(`<input type="text" pattern="[0-9]+" />`)).toContain('pattern="[0-9]+"');
|
||||||
|
|
||||||
|
const component = compile(
|
||||||
|
`component F {\n props { p: string = "x" }\n view { <input pattern="{p}" /> }\n}\n`,
|
||||||
|
"F.wrn",
|
||||||
|
).code;
|
||||||
|
expect(component).toContain("pattern");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("attributes outside the constraint set keep their empty values", () => {
|
||||||
|
// An empty value or class is meaningful and must survive untouched.
|
||||||
|
const code = render(`<input type="text" value="" class="" />`);
|
||||||
|
expect(code).toContain('value=""');
|
||||||
|
});
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { generate, parse } from "../src/index.ts";
|
||||||
|
|
||||||
|
// A `{#each}` inside a COMPONENT emitted `data-wrn-loop-locals`, so a handler
|
||||||
|
// could reference the loop variable. The same view inside a PAGE emitted the
|
||||||
|
// handler text verbatim with no locals marker, so the loop variable was not
|
||||||
|
// defined at click time and the handler threw ReferenceError -- with a green
|
||||||
|
// build and green tests, because nothing renders the page in a browser during
|
||||||
|
// a build. The runtime resolves locals with closest("[data-wrn-loop-locals]"),
|
||||||
|
// so emitting the marker is all that is required.
|
||||||
|
const view = `
|
||||||
|
{#each packs as pack}
|
||||||
|
<button @click="buyPack(pack.code)">{pack.code}</button>
|
||||||
|
{/each}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const body = `
|
||||||
|
state packs = [{ code: "small" }]
|
||||||
|
functions { client async function buyPack(c) { console.log(c) } }
|
||||||
|
view {${view}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
test("a page {#each} exposes loop locals to an event handler", () => {
|
||||||
|
const code = generate(parse(`page P {${body}}`));
|
||||||
|
expect(code).toContain("data-wrn-loop-locals");
|
||||||
|
// The item and the implicit index both have to travel, since a handler may
|
||||||
|
// reference either.
|
||||||
|
expect(code).toContain('"pack": pack');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a component {#each} still exposes loop locals to an event handler", () => {
|
||||||
|
const code = generate(parse(`component C {${body}}`));
|
||||||
|
expect(code).toContain("data-wrn-loop-locals");
|
||||||
|
expect(code).toContain('"pack": pack');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a named loop index is exposed to a page event handler", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page P {
|
||||||
|
state packs = [{ code: "small" }]
|
||||||
|
functions { client async function pick(c) { console.log(c) } }
|
||||||
|
view {
|
||||||
|
{#each packs as pack, i}
|
||||||
|
<button @click="pick(i)">{pack.code}</button>
|
||||||
|
{/each}
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).toContain('"i": i');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only elements that actually bind an event need the marker; adding it to every
|
||||||
|
// element in a loop body would bloat the HTML for no benefit.
|
||||||
|
test("a page loop body element with no handler gets no locals marker", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page P {
|
||||||
|
state packs = [{ code: "small" }]
|
||||||
|
view {
|
||||||
|
{#each packs as pack}
|
||||||
|
<span>{pack.code}</span>
|
||||||
|
{/each}
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).not.toContain("data-wrn-loop-locals");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emitting the marker without defining its encoder would turn a ReferenceError
|
||||||
|
// on the loop variable into a ReferenceError on the encoder -- at render time
|
||||||
|
// rather than click time, so strictly worse.
|
||||||
|
test("a page that emits loop locals also defines the encoder", () => {
|
||||||
|
const code = generate(parse(`page P {${body}}`));
|
||||||
|
expect(code).toContain("data-wrn-loop-locals");
|
||||||
|
expect(code).toContain("function __wrnexusEncodeLoopLocals");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a page with no loop-local markers does not define the encoder", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page P {
|
||||||
|
state packs = [{ code: "small" }]
|
||||||
|
view { <span>{packs.length}</span> }
|
||||||
|
}`),
|
||||||
|
);
|
||||||
|
expect(code).not.toContain("__wrnexusEncodeLoopLocals");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The tests above assert the marker is EMITTED. This one executes the generated
|
||||||
|
// module and asserts it RENDERS with the loop's real values -- generated text
|
||||||
|
// that reads correctly can still render wrong, and the whole point of the
|
||||||
|
// marker is what the runtime finds in the DOM at click time.
|
||||||
|
test("a page loop handler's locals render as real, decodable values", async () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(`page Packs {
|
||||||
|
state packs = [{ code: "small" }, { code: "large" }]
|
||||||
|
functions { client async function pick(c) { console.log(c) } }
|
||||||
|
view {
|
||||||
|
{#each packs as pack}
|
||||||
|
<button @click="pick(pack.code)">{pack.code}</button>
|
||||||
|
{/each}
|
||||||
|
}
|
||||||
|
}`),
|
||||||
|
).replace(/^import \{ buildApiRequest[^\n]*\n/m, "");
|
||||||
|
|
||||||
|
const js = new Bun.Transpiler({ loader: "ts" }).transformSync(code);
|
||||||
|
const mod = await import("data:text/javascript;base64," + Buffer.from(js).toString("base64"));
|
||||||
|
const html: string = await mod.default({
|
||||||
|
req: new Request("http://x/"),
|
||||||
|
cookies: {},
|
||||||
|
session: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const markers = [...html.matchAll(/data-wrn-loop-locals="([^"]+)"/g)].map((m) => m[1]!);
|
||||||
|
expect(markers.length).toBe(2);
|
||||||
|
|
||||||
|
const decoded = markers.map((m) => JSON.parse(Buffer.from(m, "base64").toString("utf8")));
|
||||||
|
expect(decoded[0].pack).toEqual({ code: "small" });
|
||||||
|
expect(decoded[1].pack).toEqual({ code: "large" });
|
||||||
|
expect(decoded[0].__wi).toBe(0);
|
||||||
|
expect(decoded[1].__wi).toBe(1);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/core",
|
"name": "@wrnexus/core",
|
||||||
"version": "0.8.10",
|
"version": "0.8.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -22,7 +22,20 @@ import {
|
|||||||
/** Translate a key for the active language, interpolating `{param}` placeholders. */
|
/** Translate a key for the active language, interpolating `{param}` placeholders. */
|
||||||
export type TFunction = (key: string, params?: Record<string, string | number>) => string;
|
export type TFunction = (key: string, params?: Record<string, string | number>) => string;
|
||||||
|
|
||||||
export type Context = {
|
export interface PaginationOptions {
|
||||||
|
defaultLimit?: number;
|
||||||
|
maxLimit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationInput {
|
||||||
|
limit: number;
|
||||||
|
cursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Context<
|
||||||
|
Params extends Record<string, string> = Record<string, string>,
|
||||||
|
User = unknown,
|
||||||
|
> {
|
||||||
/** The raw incoming web-standard Request. */
|
/** The raw incoming web-standard Request. */
|
||||||
req: Request;
|
req: Request;
|
||||||
/** Parsed URL of the request (pathname, query, etc.). */
|
/** Parsed URL of the request (pathname, query, etc.). */
|
||||||
@@ -32,17 +45,19 @@ export type Context = {
|
|||||||
/** Translate a key for the active language (identity until the runtime sets it). */
|
/** Translate a key for the active language (identity until the runtime sets it). */
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
/** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
|
/** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
|
||||||
params: Record<string, string>;
|
params: Params;
|
||||||
/**
|
/**
|
||||||
* Per-request scratch space. Middleware can attach values here
|
* Per-request scratch space. Middleware can attach values here
|
||||||
* (e.g. the authenticated user) and downstream handlers can read them.
|
* (e.g. the authenticated user) and downstream handlers can read them.
|
||||||
*/
|
*/
|
||||||
locals: Record<string, unknown>;
|
locals: Record<string, unknown>;
|
||||||
|
/** Resource resolved by a declarative route guard, when present. */
|
||||||
|
resource?: unknown;
|
||||||
/**
|
/**
|
||||||
* The authenticated user for this request, or null when anonymous. Populated
|
* The authenticated user for this request, or null when anonymous. Populated
|
||||||
* by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
|
* by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
|
||||||
*/
|
*/
|
||||||
user?: unknown;
|
user?: User | null;
|
||||||
/** Active tenant/workspace resolved by tenant middleware. */
|
/** Active tenant/workspace resolved by tenant middleware. */
|
||||||
tenant?: Tenant;
|
tenant?: Tenant;
|
||||||
/** Request tracer installed by observability middleware. */
|
/** Request tracer installed by observability middleware. */
|
||||||
@@ -59,7 +74,9 @@ export type Context = {
|
|||||||
session: SessionStore;
|
session: SessionStore;
|
||||||
/** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
|
/** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
|
||||||
localStorage: LocalStorageSnapshot;
|
localStorage: LocalStorageSnapshot;
|
||||||
};
|
/** Parse and bound the standard `limit` and `cursor` query parameters. */
|
||||||
|
pagination(options?: PaginationOptions): PaginationInput;
|
||||||
|
}
|
||||||
|
|
||||||
/** Calls the next middleware in the chain (or the final route handler). */
|
/** Calls the next middleware in the chain (or the final route handler). */
|
||||||
export type Next = () => Promise<Response> | Response;
|
export type Next = () => Promise<Response> | Response;
|
||||||
@@ -99,12 +116,15 @@ export type PageMeta = SeoConfig;
|
|||||||
export type PageComponent = (ctx: Context) => string | Promise<string>;
|
export type PageComponent = (ctx: Context) => string | Promise<string>;
|
||||||
|
|
||||||
/** Create a fresh context for an incoming request. */
|
/** Create a fresh context for an incoming request. */
|
||||||
export function createContext(req: Request, url: URL): Context {
|
export function createContext<Params extends Record<string, string> = Record<string, string>>(
|
||||||
|
req: Request,
|
||||||
|
url: URL,
|
||||||
|
): Context<Params> {
|
||||||
const cookies = createCookieStore(req);
|
const cookies = createCookieStore(req);
|
||||||
return {
|
return {
|
||||||
req,
|
req,
|
||||||
url,
|
url,
|
||||||
params: {},
|
params: {} as Params,
|
||||||
locals: {},
|
locals: {},
|
||||||
lang: "",
|
lang: "",
|
||||||
t: (key) => key,
|
t: (key) => key,
|
||||||
@@ -113,9 +133,24 @@ export function createContext(req: Request, url: URL): Context {
|
|||||||
// cookies get `Secure` behind a TLS-terminating proxy (matches CSRF cookies).
|
// cookies get `Secure` behind a TLS-terminating proxy (matches CSRF cookies).
|
||||||
session: createSessionStore(cookies, req, undefined, url.protocol === "https:"),
|
session: createSessionStore(cookies, req, undefined, url.protocol === "https:"),
|
||||||
localStorage: createLocalStorageSnapshot(req),
|
localStorage: createLocalStorageSnapshot(req),
|
||||||
|
pagination(options = {}) {
|
||||||
|
const defaultLimit = positivePaginationInteger(options.defaultLimit, 25);
|
||||||
|
const maxLimit = positivePaginationInteger(options.maxLimit, 100);
|
||||||
|
const requested = Number(url.searchParams.get("limit"));
|
||||||
|
const limit =
|
||||||
|
Number.isInteger(requested) && requested > 0
|
||||||
|
? Math.min(requested, maxLimit)
|
||||||
|
: Math.min(defaultLimit, maxLimit);
|
||||||
|
const cursor = url.searchParams.get("cursor")?.trim() || undefined;
|
||||||
|
return { limit, cursor };
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positivePaginationInteger(value: number | undefined, fallback: number): number {
|
||||||
|
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
/** Apply headers accumulated on the context, such as Set-Cookie. */
|
/** Apply headers accumulated on the context, such as Set-Cookie. */
|
||||||
export function withContextHeaders(ctx: Context, res: Response): Response {
|
export function withContextHeaders(ctx: Context, res: Response): Response {
|
||||||
const headers = new Headers(res.headers);
|
const headers = new Headers(res.headers);
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export interface EndpointDefinition<I, O> {
|
|||||||
input?: SchemaLike<I> | OutputSchemaLike<I>;
|
input?: SchemaLike<I> | OutputSchemaLike<I>;
|
||||||
output?: SchemaLike<O> | OutputSchemaLike<O>;
|
output?: SchemaLike<O> | OutputSchemaLike<O>;
|
||||||
auth?: "optional" | "required";
|
auth?: "optional" | "required";
|
||||||
|
/** Permission checked through an installed @wrnexus/authz middleware. */
|
||||||
|
permission?: string;
|
||||||
|
/** Resource supplied to bound authorization policies. */
|
||||||
|
resource?: (ctx: Context, input: I) => unknown | Promise<unknown>;
|
||||||
description?: string;
|
description?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
handler(input: I, ctx: Context): O | Promise<O>;
|
handler(input: I, ctx: Context): O | Promise<O>;
|
||||||
@@ -111,6 +115,31 @@ export function defineEndpoint(
|
|||||||
: await ctx.req.json().catch(() => ({}));
|
: await ctx.req.json().catch(() => ({}));
|
||||||
input = schemaValue(definition.input, resolvedInput);
|
input = schemaValue(definition.input, resolvedInput);
|
||||||
}
|
}
|
||||||
|
if (definition.permission) {
|
||||||
|
const authz = (
|
||||||
|
ctx as Context & {
|
||||||
|
authz?: {
|
||||||
|
decide(
|
||||||
|
permission: string,
|
||||||
|
resource?: unknown,
|
||||||
|
): Promise<{ allowed: boolean; reason?: string }>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
).authz;
|
||||||
|
if (!authz) {
|
||||||
|
throw new EndpointError(
|
||||||
|
500,
|
||||||
|
"AUTHZ_NOT_CONFIGURED",
|
||||||
|
"Authorization middleware is not configured.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const resource = definition.resource ? await definition.resource(ctx, input) : undefined;
|
||||||
|
const decision = await authz.decide(definition.permission, resource);
|
||||||
|
if (!decision.allowed) {
|
||||||
|
throw new EndpointError(403, "FORBIDDEN", "Permission denied.");
|
||||||
|
}
|
||||||
|
ctx.resource = resource;
|
||||||
|
}
|
||||||
const rawOutput = await definition.handler(input, ctx);
|
const rawOutput = await definition.handler(input, ctx);
|
||||||
const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput;
|
const output = definition.output ? schemaValue(definition.output, rawOutput) : rawOutput;
|
||||||
return output instanceof Response ? output : json({ data: output });
|
return output instanceof Response ? output : json({ data: output });
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ export type {
|
|||||||
PageComponent,
|
PageComponent,
|
||||||
SeoConfig,
|
SeoConfig,
|
||||||
TFunction,
|
TFunction,
|
||||||
|
PaginationOptions,
|
||||||
|
PaginationInput,
|
||||||
} from "./context.ts";
|
} from "./context.ts";
|
||||||
export { createContext, withContextHeaders } from "./context.ts";
|
export { createContext, withContextHeaders } from "./context.ts";
|
||||||
export {
|
export {
|
||||||
@@ -127,6 +129,7 @@ export {
|
|||||||
export type {
|
export type {
|
||||||
CookieOptions,
|
CookieOptions,
|
||||||
CookieStore,
|
CookieStore,
|
||||||
|
TransactionCookie,
|
||||||
LocalStorageSnapshot,
|
LocalStorageSnapshot,
|
||||||
SessionStore,
|
SessionStore,
|
||||||
SessionBackend,
|
SessionBackend,
|
||||||
|
|||||||
@@ -17,6 +17,16 @@ export interface CookieStore {
|
|||||||
set(name: string, value: string, options?: CookieOptions): void;
|
set(name: string, value: string, options?: CookieOptions): void;
|
||||||
delete(name: string, options?: CookieOptions): void;
|
delete(name: string, options?: CookieOptions): void;
|
||||||
headers(): string[];
|
headers(): string[];
|
||||||
|
transaction<T extends Record<string, unknown> = Record<string, unknown>>(
|
||||||
|
name: string,
|
||||||
|
options?: CookieOptions,
|
||||||
|
): TransactionCookie<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionCookie<T extends Record<string, unknown>> {
|
||||||
|
set(value: T): void;
|
||||||
|
consume(): T | undefined;
|
||||||
|
clear(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionStore {
|
export interface SessionStore {
|
||||||
@@ -259,7 +269,7 @@ export function createCookieStore(req: Request): CookieStore {
|
|||||||
const incoming = parseCookieHeader(req.headers.get("cookie") ?? "");
|
const incoming = parseCookieHeader(req.headers.get("cookie") ?? "");
|
||||||
const outgoing: string[] = [];
|
const outgoing: string[] = [];
|
||||||
|
|
||||||
return {
|
const store: CookieStore = {
|
||||||
get(name) {
|
get(name) {
|
||||||
return incoming[name];
|
return incoming[name];
|
||||||
},
|
},
|
||||||
@@ -287,7 +297,45 @@ export function createCookieStore(req: Request): CookieStore {
|
|||||||
headers() {
|
headers() {
|
||||||
return [...outgoing];
|
return [...outgoing];
|
||||||
},
|
},
|
||||||
|
transaction<T extends Record<string, unknown>>(name: string, options: CookieOptions = {}) {
|
||||||
|
const policy: CookieOptions = {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "Lax",
|
||||||
|
maxAge: 600,
|
||||||
|
secure: new URL(req.url).protocol === "https:",
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
set(value: T) {
|
||||||
|
const json = JSON.stringify(value);
|
||||||
|
const encoded = btoa(unescape(encodeURIComponent(json)))
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
store.set(name, encoded, policy);
|
||||||
|
},
|
||||||
|
consume(): T | undefined {
|
||||||
|
const encoded = store.get(name);
|
||||||
|
store.delete(name, policy);
|
||||||
|
if (!encoded) return undefined;
|
||||||
|
try {
|
||||||
|
const padded = encoded
|
||||||
|
.replace(/-/g, "+")
|
||||||
|
.replace(/_/g, "/")
|
||||||
|
.padEnd(Math.ceil(encoded.length / 4) * 4, "=");
|
||||||
|
return JSON.parse(decodeURIComponent(escape(atob(padded)))) as T;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
store.delete(name, policy);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSessionStore(
|
export function createSessionStore(
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { createContext } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("context pagination applies defaults, bounds and cursor parsing", () => {
|
||||||
|
const request = new Request("https://example.test/items?limit=500&cursor=next-1");
|
||||||
|
const ctx = createContext(request, new URL(request.url));
|
||||||
|
expect(ctx.pagination({ defaultLimit: 25, maxLimit: 100 })).toEqual({
|
||||||
|
limit: 100,
|
||||||
|
cursor: "next-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("context pagination rejects invalid limits", () => {
|
||||||
|
const request = new Request("https://example.test/items?limit=-2");
|
||||||
|
const ctx = createContext(request, new URL(request.url));
|
||||||
|
expect(ctx.pagination()).toEqual({ limit: 25, cursor: undefined });
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { createContext, withContextHeaders } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("transaction cookies preserve policy when consumed and delete exactly once", () => {
|
||||||
|
const firstRequest = new Request("https://example.test/oauth");
|
||||||
|
const first = createContext(firstRequest, new URL(firstRequest.url));
|
||||||
|
first.cookies.transaction<{ state: string }>("oauth").set({ state: "välue" });
|
||||||
|
const issued = withContextHeaders(first, new Response()).headers.get("set-cookie")!;
|
||||||
|
expect(issued).toContain("HttpOnly");
|
||||||
|
expect(issued).toContain("SameSite=Lax");
|
||||||
|
expect(issued).toContain("Secure");
|
||||||
|
|
||||||
|
const pair = issued.split(";", 1)[0]!;
|
||||||
|
const callbackRequest = new Request("https://example.test/callback", {
|
||||||
|
headers: { cookie: pair },
|
||||||
|
});
|
||||||
|
const callback = createContext(callbackRequest, new URL(callbackRequest.url));
|
||||||
|
expect(callback.cookies.transaction<{ state: string }>("oauth").consume()).toEqual({
|
||||||
|
state: "välue",
|
||||||
|
});
|
||||||
|
const consumed = withContextHeaders(callback, new Response()).headers.get("set-cookie")!;
|
||||||
|
expect(consumed).toContain("Max-Age=0");
|
||||||
|
expect(consumed).toContain("Path=/");
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/csr",
|
"name": "@wrnexus/csr",
|
||||||
"version": "0.8.25",
|
"version": "0.8.35",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
export interface ApiRequest<Input = unknown> {
|
||||||
|
method?: string;
|
||||||
|
params?: Record<string, string | number>;
|
||||||
|
query?: Record<string, string | number | boolean | undefined>;
|
||||||
|
body?: Input;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiClientOptions {
|
||||||
|
baseUrl?: string;
|
||||||
|
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Runtime used by generated API clients; throws a typed response error on non-2xx results. */
|
||||||
|
export function createApiClient(options: ApiClientOptions = {}) {
|
||||||
|
const fetcher = options.fetch ?? globalThis.fetch;
|
||||||
|
return async function call<Output, Input = unknown>(
|
||||||
|
template: string,
|
||||||
|
request: ApiRequest<Input> = {},
|
||||||
|
): Promise<Output> {
|
||||||
|
let path = template;
|
||||||
|
for (const [name, value] of Object.entries(request.params ?? {})) {
|
||||||
|
path = path.replace(`[${name}]`, encodeURIComponent(String(value)));
|
||||||
|
}
|
||||||
|
const url = new URL(path, options.baseUrl ?? globalThis.location?.origin ?? "http://localhost");
|
||||||
|
for (const [name, value] of Object.entries(request.query ?? {}))
|
||||||
|
if (value !== undefined) url.searchParams.set(name, String(value));
|
||||||
|
const response = await fetcher(url, {
|
||||||
|
method: request.method ?? (request.body === undefined ? "GET" : "POST"),
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: request.body === undefined ? undefined : { "content-type": "application/json" },
|
||||||
|
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
||||||
|
});
|
||||||
|
const data = response.status === 204 ? undefined : await response.json();
|
||||||
|
if (!response.ok)
|
||||||
|
throw Object.assign(
|
||||||
|
new Error((data as { error?: string })?.error ?? `HTTP ${response.status}`),
|
||||||
|
{ status: response.status, data },
|
||||||
|
);
|
||||||
|
return data as Output;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -16,6 +16,23 @@ export { REACTIVE_RUNTIME } from "./reactive-runtime.ts";
|
|||||||
export { NAV_RUNTIME } from "./nav-runtime.ts";
|
export { NAV_RUNTIME } from "./nav-runtime.ts";
|
||||||
export { REALTIME_RUNTIME } from "./realtime-runtime.ts";
|
export { REALTIME_RUNTIME } from "./realtime-runtime.ts";
|
||||||
export { ACTION_RUNTIME } from "./action-runtime.ts";
|
export { ACTION_RUNTIME } from "./action-runtime.ts";
|
||||||
|
export { createApiClient } from "./api-client.ts";
|
||||||
|
export type { ApiClientOptions, ApiRequest } from "./api-client.ts";
|
||||||
|
|
||||||
|
/** Public client transport used by `.wrn` client functions for dynamic URLs. */
|
||||||
|
export function useFetch<T = unknown>(
|
||||||
|
path: string,
|
||||||
|
methodOrOptions?:
|
||||||
|
string | { method?: string; query?: unknown; params?: unknown; body?: unknown; data?: unknown },
|
||||||
|
input?: unknown,
|
||||||
|
): Promise<T> {
|
||||||
|
const client = (
|
||||||
|
globalThis as typeof globalThis & { useFetch?: (...args: unknown[]) => Promise<T> }
|
||||||
|
).useFetch;
|
||||||
|
if (!client)
|
||||||
|
return Promise.reject(new Error("WRN-CSR: useFetch is only available in the browser runtime"));
|
||||||
|
return client(path, methodOrOptions, input);
|
||||||
|
}
|
||||||
|
|
||||||
const CONTROLLER_SECTIONS = ["PRIMARY", "UI", "PIN"] as const;
|
const CONTROLLER_SECTIONS = ["PRIMARY", "UI", "PIN"] as const;
|
||||||
|
|
||||||
@@ -73,7 +90,7 @@ const CONTROLLER_LOADER = String.raw`
|
|||||||
if (!componentControllerPromise) {
|
if (!componentControllerPromise) {
|
||||||
componentControllerPromise = new Promise(function (resolve, reject) {
|
componentControllerPromise = new Promise(function (resolve, reject) {
|
||||||
var script = document.createElement("script");
|
var script = document.createElement("script");
|
||||||
script.src = componentControllerUrl;
|
script.src = trustedScriptURL(componentControllerUrl);
|
||||||
script.defer = true;
|
script.defer = true;
|
||||||
script.onload = resolve;
|
script.onload = resolve;
|
||||||
script.onerror = reject;
|
script.onerror = reject;
|
||||||
|
|||||||
@@ -26,6 +26,40 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var behaviorObserver;
|
var behaviorObserver;
|
||||||
var clientModuleCache = new Map();
|
var clientModuleCache = new Map();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Production enables require-trusted-types-for 'script'. Every HTML write
|
||||||
|
* below uses compiler-emitted template markup (or the explicit data-html
|
||||||
|
* escape hatch), and controller URLs are generated by the framework. Route
|
||||||
|
* those known sinks through the policy allowed by the default security
|
||||||
|
* preset instead of letting the browser reject hydration midway through a
|
||||||
|
* state update.
|
||||||
|
*/
|
||||||
|
var wrnexusTrustedTypesPolicy = null;
|
||||||
|
if (window.trustedTypes && typeof window.trustedTypes.createPolicy === "function") {
|
||||||
|
try {
|
||||||
|
wrnexusTrustedTypesPolicy = window.trustedTypes.createPolicy("wrnexus", {
|
||||||
|
createHTML: function (value) { return value; },
|
||||||
|
createScriptURL: function (value) { return value; }
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
wrnexusTrustedTypesPolicy = window.trustedTypes.defaultPolicy || null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function trustedHTML(value) {
|
||||||
|
var source = value == null ? "" : String(value);
|
||||||
|
return wrnexusTrustedTypesPolicy
|
||||||
|
? wrnexusTrustedTypesPolicy.createHTML(source)
|
||||||
|
: source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trustedScriptURL(value) {
|
||||||
|
var source = value == null ? "" : String(value);
|
||||||
|
return wrnexusTrustedTypesPolicy
|
||||||
|
? wrnexusTrustedTypesPolicy.createScriptURL(source)
|
||||||
|
: source;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Two builtin chains the runtime reaches for constantly. Aliasing them is
|
* Two builtin chains the runtime reaches for constantly. Aliasing them is
|
||||||
* not only shorter: hasOwn keeps prototype keys from reading as data, and
|
* not only shorter: hasOwn keeps prototype keys from reading as data, and
|
||||||
@@ -234,8 +268,21 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
Array.prototype.forEach.call(rules || [], function (rule) {
|
Array.prototype.forEach.call(rules || [], function (rule) {
|
||||||
var cssText = rule.cssText || "";
|
var cssText = rule.cssText || "";
|
||||||
var match;
|
var match;
|
||||||
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)/g;
|
// Only inspect declarations that can affect the current document. A
|
||||||
while ((match = pattern.exec(cssText))) referenced[match[1]] = true;
|
// component stylesheet may contain optional variants that are not
|
||||||
|
// rendered on this page, and variables with fallbacks are intentionally
|
||||||
|
// optional rather than missing theme tokens.
|
||||||
|
if (rule.selectorText) {
|
||||||
|
try {
|
||||||
|
if (!document.querySelector(rule.selectorText)) return;
|
||||||
|
} catch (_) {
|
||||||
|
// Keep inspecting selectors that the DOM implementation cannot parse.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)\s*(,)?/g;
|
||||||
|
while ((match = pattern.exec(cssText))) {
|
||||||
|
if (!match[2]) referenced[match[1]] = true;
|
||||||
|
}
|
||||||
if (rule.style) {
|
if (rule.style) {
|
||||||
Array.prototype.forEach.call(rule.style, function (property) {
|
Array.prototype.forEach.call(rule.style, function (property) {
|
||||||
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
|
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
|
||||||
@@ -980,6 +1027,34 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
renderers[index]();
|
renderers[index]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* Write loop locals onto a client-rendered item, mirroring the
|
||||||
|
* data-wrn-loop-locals attribute the server emits for an each block.
|
||||||
|
*
|
||||||
|
* Client data-for used to pass locals to hydration in memory only, so
|
||||||
|
* anything that resolves them by READING the DOM -- a component output
|
||||||
|
* binding calls decodeLoopLocals(componentRoot) -- found nothing and
|
||||||
|
* silently dropped the value. A plain DOM handler kept working, because it
|
||||||
|
* receives locals through the hydration closure, which is what made the
|
||||||
|
* failure look arbitrary. The DOM is the single source of truth for
|
||||||
|
* locals; both paths write it now.
|
||||||
|
*/
|
||||||
|
function writeLoopLocals(node, locals) {
|
||||||
|
if (!node || node.nodeType !== 1 || !locals) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var json = JSON.stringify(locals);
|
||||||
|
if (json === undefined) return;
|
||||||
|
|
||||||
|
node.setAttribute(
|
||||||
|
"data-wrn-loop-locals",
|
||||||
|
window.btoa(unescape(encodeURIComponent(json))),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[wrnexus] failed to encode loop locals", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function decodeLoopLocals(node) {
|
function decodeLoopLocals(node) {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return {};
|
return {};
|
||||||
@@ -1145,6 +1220,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
* lacks one of these does not break the rest.
|
* lacks one of these does not break the rest.
|
||||||
*/
|
*/
|
||||||
if (name === "toast") return toastApi;
|
if (name === "toast") return toastApi;
|
||||||
|
if (name === "useFetch") return useFetch;
|
||||||
if (boundWindowGlobals[name] && typeof window[name] === "function") {
|
if (boundWindowGlobals[name] && typeof window[name] === "function") {
|
||||||
return window[name].bind(window);
|
return window[name].bind(window);
|
||||||
}
|
}
|
||||||
@@ -1789,7 +1865,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var next = markup == null ? "" : String(markup);
|
var next = markup == null ? "" : String(markup);
|
||||||
|
|
||||||
if (htmlNode.innerHTML !== next) {
|
if (htmlNode.innerHTML !== next) {
|
||||||
htmlNode.innerHTML = next;
|
htmlNode.innerHTML = trustedHTML(next);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
runHtml();
|
runHtml();
|
||||||
@@ -1973,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearControlContent() {
|
function clearControlContent() {
|
||||||
if (!rangeEnd) { block.innerHTML = ""; return; }
|
if (!rangeEnd) { block.innerHTML = trustedHTML(""); return; }
|
||||||
while (block.nextSibling && block.nextSibling !== rangeEnd) {
|
while (block.nextSibling && block.nextSibling !== rangeEnd) {
|
||||||
block.parentNode.removeChild(block.nextSibling);
|
block.parentNode.removeChild(block.nextSibling);
|
||||||
}
|
}
|
||||||
@@ -1981,7 +2057,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
|
|
||||||
function appendControlContent(markup, locals) {
|
function appendControlContent(markup, locals) {
|
||||||
var template = document.createElement("template");
|
var template = document.createElement("template");
|
||||||
template.innerHTML = markup || "";
|
template.innerHTML = trustedHTML(markup || "");
|
||||||
var fragment = template.content;
|
var fragment = template.content;
|
||||||
var elements = toArray(fragment.childNodes).filter(function (node) {
|
var elements = toArray(fragment.childNodes).filter(function (node) {
|
||||||
return node.nodeType === 1;
|
return node.nodeType === 1;
|
||||||
@@ -2216,6 +2292,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
itemIndex;
|
itemIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeLoopLocals(clone, locals);
|
||||||
|
|
||||||
hydrateItem(
|
hydrateItem(
|
||||||
clone,
|
clone,
|
||||||
locals,
|
locals,
|
||||||
@@ -2292,6 +2370,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
}
|
}
|
||||||
|
|
||||||
var keyedClone = template.cloneNode(true);
|
var keyedClone = template.cloneNode(true);
|
||||||
|
writeLoopLocals(keyedClone, keyedLocals);
|
||||||
hydrateItem(keyedClone, keyedLocals);
|
hydrateItem(keyedClone, keyedLocals);
|
||||||
record = {
|
record = {
|
||||||
node: keyedClone,
|
node: keyedClone,
|
||||||
@@ -2367,7 +2446,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
var markup = value == null ? "" : String(value);
|
var markup = value == null ? "" : String(value);
|
||||||
|
|
||||||
if (node.innerHTML !== markup) {
|
if (node.innerHTML !== markup) {
|
||||||
node.innerHTML = markup;
|
node.innerHTML = trustedHTML(markup);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2724,9 +2803,17 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
node.addEventListener(outName, domListener);
|
/*
|
||||||
|
* focus and blur do not bubble, so a bubble-phase listener on the
|
||||||
|
* component root never hears its own input or button take focus --
|
||||||
|
* the binding silently did nothing. Capture reaches the descendant.
|
||||||
|
* The same flag must be passed to removeEventListener or the
|
||||||
|
* listener outlives the component.
|
||||||
|
*/
|
||||||
|
var capture = outName === "focus" || outName === "blur";
|
||||||
|
node.addEventListener(outName, domListener, capture);
|
||||||
cleanupCallbacks.push(function () {
|
cleanupCallbacks.push(function () {
|
||||||
node.removeEventListener(outName, domListener);
|
node.removeEventListener(outName, domListener, capture);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2735,9 +2822,15 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
// server forwards these markers onto the rendered child root; evaluate
|
// server forwards these markers onto the rendered child root; evaluate
|
||||||
// them here and write changes into the child's prop signals.
|
// them here and write changes into the child's prop signals.
|
||||||
toArray(el.querySelectorAll("*"))
|
toArray(el.querySelectorAll("*"))
|
||||||
.filter(isScopeRoot)
|
.filter(function (node) {
|
||||||
|
return toArray(node.attributes).some(function (attr) {
|
||||||
|
return attr.name.indexOf("data-wrn-prop-bind-") === 0;
|
||||||
|
});
|
||||||
|
})
|
||||||
.forEach(function (node) {
|
.forEach(function (node) {
|
||||||
if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
|
var targetScope = isScopeRoot(node) ? node : ownerScope(node);
|
||||||
|
if (!targetScope || targetScope === el) return;
|
||||||
|
if (!targetScope.parentNode || ownerScope(targetScope.parentNode) !== el) return;
|
||||||
toArray(node.attributes).forEach(function (attr) {
|
toArray(node.attributes).forEach(function (attr) {
|
||||||
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
|
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
|
||||||
var binding = pairBinding(attr.value);
|
var binding = pairBinding(attr.value);
|
||||||
@@ -2756,9 +2849,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
});
|
});
|
||||||
} catch (_) { return; }
|
} catch (_) { return; }
|
||||||
var apply = function () {
|
var apply = function () {
|
||||||
if (node.__wrnexusScopeApi) node.__wrnexusScopeApi.set(propName, value);
|
if (targetScope.__wrnexusScopeApi) targetScope.__wrnexusScopeApi.set(propName, value);
|
||||||
};
|
};
|
||||||
if (node.__wrnexusScopeApi) apply();
|
if (targetScope.__wrnexusScopeApi) apply();
|
||||||
else queueMicrotask(apply);
|
else queueMicrotask(apply);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -4311,6 +4404,29 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public client transport for dynamic API paths.
|
||||||
|
*
|
||||||
|
* The apis block remains the typed, declarative choice for fixed routes. This
|
||||||
|
* helper covers paths assembled at runtime without exposing a private
|
||||||
|
* framework global to application code.
|
||||||
|
*/
|
||||||
|
function useFetch(path, methodOrOptions, input) {
|
||||||
|
var method = "GET";
|
||||||
|
var values = input;
|
||||||
|
if (typeof methodOrOptions === "string") {
|
||||||
|
method = methodOrOptions;
|
||||||
|
} else if (methodOrOptions && typeof methodOrOptions === "object") {
|
||||||
|
method = methodOrOptions.method || "GET";
|
||||||
|
values = method === "GET" || method === "HEAD"
|
||||||
|
? (methodOrOptions.query || methodOrOptions.params)
|
||||||
|
: (methodOrOptions.body === undefined ? methodOrOptions.data : methodOrOptions.body);
|
||||||
|
}
|
||||||
|
return wrnexusCallApi(path, method, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.useFetch = useFetch;
|
||||||
|
// Compatibility for applications compiled before the public helper landed.
|
||||||
window.__wrnexusCallApi = wrnexusCallApi;
|
window.__wrnexusCallApi = wrnexusCallApi;
|
||||||
|
|
||||||
function dispatchComponentEvent(root, name, detail) {
|
function dispatchComponentEvent(root, name, detail) {
|
||||||
|
|||||||
@@ -64,6 +64,21 @@ test("POST sends a JSON body", async () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("useFetch is the public dynamic-path transport", async () => {
|
||||||
|
const { calls, win } = harness({ status: 200, payload: { ok: true } });
|
||||||
|
const useFetch = (
|
||||||
|
win as unknown as {
|
||||||
|
useFetch: (path: string, options: { method: string; body: unknown }) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
).useFetch;
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
useFetch("/api/users/42/test", { method: "POST", body: { dryRun: true } }),
|
||||||
|
).resolves.toEqual({ ok: true });
|
||||||
|
expect(calls[0]!.url).toBe("/api/users/42/test");
|
||||||
|
expect(calls[0]!.init.body).toBe(JSON.stringify({ dryRun: true }));
|
||||||
|
});
|
||||||
|
|
||||||
test("a non-GET request carries the CSRF token from the cookie", async () => {
|
test("a non-GET request carries the CSRF token from the cookie", async () => {
|
||||||
const { callApi, calls, win } = harness({ status: 200, payload: {} });
|
const { callApi, calls, win } = harness({ status: 200, payload: {} });
|
||||||
win.document.cookie = "wrn-csrf=token-123";
|
win.document.cookie = "wrn-csrf=token-123";
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { createApiClient } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("typed API client expands params, query and JSON input", async () => {
|
||||||
|
let request: Request | undefined;
|
||||||
|
const call = createApiClient({
|
||||||
|
baseUrl: "https://app.test",
|
||||||
|
fetch: async (input, init) => {
|
||||||
|
request = new Request(input, init);
|
||||||
|
return Response.json({ ok: true });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await call<{ ok: boolean }, { name: string }>("/api/users/[id]", {
|
||||||
|
method: "PUT",
|
||||||
|
params: { id: 7 },
|
||||||
|
query: { audit: true },
|
||||||
|
body: { name: "Ada" },
|
||||||
|
}),
|
||||||
|
).toEqual({ ok: true });
|
||||||
|
expect(request?.url).toBe("https://app.test/api/users/7?audit=true");
|
||||||
|
expect(await request?.json()).toEqual({ name: "Ada" });
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { Window } from "happy-dom";
|
||||||
|
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
|
||||||
|
import { restoreGlobalsAfterAll } from "./global-restore.ts";
|
||||||
|
|
||||||
|
restoreGlobalsAfterAll([
|
||||||
|
"window",
|
||||||
|
"document",
|
||||||
|
"location",
|
||||||
|
"NodeFilter",
|
||||||
|
"MutationObserver",
|
||||||
|
"CustomEvent",
|
||||||
|
]);
|
||||||
|
|
||||||
|
function mount(html: string): Window {
|
||||||
|
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||||
|
win.document.body.innerHTML = `<div id="app">${html}</div>`;
|
||||||
|
const g = globalThis as Record<string, unknown>;
|
||||||
|
g.window = win;
|
||||||
|
g.document = win.document;
|
||||||
|
g.location = win.location;
|
||||||
|
g.NodeFilter = (win as unknown as { NodeFilter: unknown }).NodeFilter;
|
||||||
|
g.MutationObserver = (win as unknown as { MutationObserver: unknown }).MutationObserver;
|
||||||
|
g.CustomEvent = (win as unknown as { CustomEvent: unknown }).CustomEvent;
|
||||||
|
(0, eval)(REACTIVE_RUNTIME);
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A client-rendered `data-for` passed its loop locals to hydration in memory but
|
||||||
|
// never wrote the `data-wrn-loop-locals` marker the SSR path writes. Anything
|
||||||
|
// that resolves locals by READING the DOM -- notably a component's
|
||||||
|
// `data-wrn-out-*` output binding, which calls decodeLoopLocals(componentRoot)
|
||||||
|
// -- therefore found nothing and silently dropped the value, with no console
|
||||||
|
// error. A plain DOM `@click` kept working, because it receives locals through
|
||||||
|
// the hydration closure instead, which is what made this look arbitrary.
|
||||||
|
test("a client-rendered for-loop item carries its loop locals in the DOM", () => {
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="items: [{code: 'small'}, {code: 'large'}]">
|
||||||
|
<div data-for="item in items" data-component="Card" data-wrn-out-action="pick(item.code)"></div>
|
||||||
|
</div>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const mounts = win.document.querySelectorAll("[data-component='Card']");
|
||||||
|
expect(mounts.length).toBe(2);
|
||||||
|
|
||||||
|
const decoded = Array.from(mounts).map((node) => {
|
||||||
|
const raw = (node as unknown as Element).getAttribute("data-wrn-loop-locals");
|
||||||
|
expect(raw).toBeTruthy();
|
||||||
|
return JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// The marker must carry the real item, so an output binding naming `item`
|
||||||
|
// resolves it rather than silently dropping the call.
|
||||||
|
expect(decoded[0].item).toEqual({ code: "small" });
|
||||||
|
expect(decoded[1].item).toEqual({ code: "large" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// The keyed path builds its clones separately, so it needs its own coverage --
|
||||||
|
// a fix applied to only one of the two loop paths leaves half the bug in place.
|
||||||
|
test("a keyed for-loop item carries its loop locals too", () => {
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="rows: [{id: 1, code: 'a'}, {id: 2, code: 'b'}]">
|
||||||
|
<div data-for="row in rows key row.id" data-component="Card" data-wrn-out-action="pick(row.code)"></div>
|
||||||
|
</div>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decoded = Array.from(win.document.querySelectorAll("[data-component='Card']"), (node) => {
|
||||||
|
const raw = (node as unknown as Element).getAttribute("data-wrn-loop-locals");
|
||||||
|
expect(raw).toBeTruthy();
|
||||||
|
return JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(decoded.map((d) => d.row.code)).toEqual(["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The encoder goes through UTF-8 before base64, as the server's does. btoa on a
|
||||||
|
// raw JS string throws on any character above U+00FF, which would take out the
|
||||||
|
// whole loop for an ordinary non-ASCII label.
|
||||||
|
test("loop locals survive non-ASCII values", () => {
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="rows: [{label: 'Ünïcode — 日本語'}]">
|
||||||
|
<div data-for="row in rows" data-component="Card" data-wrn-out-action="pick(row.label)"></div>
|
||||||
|
</div>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const raw = win.document
|
||||||
|
.querySelector("[data-component='Card']")!
|
||||||
|
.getAttribute("data-wrn-loop-locals");
|
||||||
|
expect(raw).toBeTruthy();
|
||||||
|
const decoded = JSON.parse(Buffer.from(String(raw), "base64").toString("utf8"));
|
||||||
|
expect(decoded.row.label).toBe("Ünïcode — 日本語");
|
||||||
|
});
|
||||||
@@ -62,6 +62,13 @@ test("split runtime hydrates a controller only from the controller asset", () =>
|
|||||||
expect(win.document.activeElement).toBe(buttons[1]);
|
expect(win.document.activeElement).toBe(buttons[1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("production hydration uses Trusted Types for HTML and controller URLs", () => {
|
||||||
|
const runtime = getReactiveRuntime();
|
||||||
|
expect(runtime).toContain('createPolicy("wrnexus"');
|
||||||
|
expect(runtime).toContain('template.innerHTML = trustedHTML(markup || "")');
|
||||||
|
expect(runtime).toContain("script.src = trustedScriptURL(componentControllerUrl)");
|
||||||
|
});
|
||||||
|
|
||||||
test("a page without controller markers does not request the controller asset", () => {
|
test("a page without controller markers does not request the controller asset", () => {
|
||||||
const win = mount(
|
const win = mount(
|
||||||
`<main data-scope="count: 1"><span>{count}</span></main>`,
|
`<main data-scope="count: 1"><span>{count}</span></main>`,
|
||||||
@@ -454,6 +461,22 @@ test("parent state updates a mounted child's reactive prop", async () => {
|
|||||||
expect(win.document.querySelector("#child-value")?.textContent).toBe("2");
|
expect(win.document.querySelector("#child-value")?.textContent).toBe("2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a prop marker forwarded onto a child control still updates the child scope", async () => {
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="enabled: false">` +
|
||||||
|
`<button data-on-click="enabled = true">enable</button>` +
|
||||||
|
`<div data-scope="disabled: true">` +
|
||||||
|
`<button id="child" disabled data-wrn-bind-0='["disabled","{disabled}"]' ` +
|
||||||
|
`data-wrn-prop-bind-0='["disabled","{!enabled}"]'>child</button>` +
|
||||||
|
`</div></div>`,
|
||||||
|
);
|
||||||
|
await Promise.resolve();
|
||||||
|
const buttons = win.document.querySelectorAll("button");
|
||||||
|
expect((buttons[1] as unknown as HTMLButtonElement).disabled).toBe(true);
|
||||||
|
(buttons[0] as unknown as HTMLElement).click();
|
||||||
|
expect((buttons[1] as unknown as HTMLButtonElement).disabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
test("independent signals in one scope update correctly (dependency tracking)", () => {
|
test("independent signals in one scope update correctly (dependency tracking)", () => {
|
||||||
const win = mount(
|
const win = mount(
|
||||||
`<div data-scope="a: 0, b: 100">
|
`<div data-scope="a: 0, b: 100">
|
||||||
@@ -1065,6 +1088,38 @@ test("development runtime accepts component-local and inline wrn variables", ()
|
|||||||
expect(warnings).toHaveLength(0);
|
expect(warnings).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("development runtime ignores fallback and inactive component variables", () => {
|
||||||
|
const win = new Window() as unknown as Window & Record<string, unknown>;
|
||||||
|
win.document.head.innerHTML =
|
||||||
|
`<style>.visible { color: var(--wrn-optional, red); } ` +
|
||||||
|
`.not-rendered { color: var(--wrn-inactive); }</style>`;
|
||||||
|
win.document.body.innerHTML = `<div class="visible" data-scope=""></div>`;
|
||||||
|
(globalThis as Record<string, unknown>).window = win;
|
||||||
|
(globalThis as Record<string, unknown>).document = win.document;
|
||||||
|
(globalThis as Record<string, unknown>).location = win.location;
|
||||||
|
(globalThis as Record<string, unknown>).NodeFilter = (
|
||||||
|
win as unknown as { NodeFilter: unknown }
|
||||||
|
).NodeFilter;
|
||||||
|
(globalThis as Record<string, unknown>).MutationObserver = (
|
||||||
|
win as unknown as { MutationObserver: unknown }
|
||||||
|
).MutationObserver;
|
||||||
|
(globalThis as Record<string, unknown>).CustomEvent = (
|
||||||
|
win as unknown as { CustomEvent: unknown }
|
||||||
|
).CustomEvent;
|
||||||
|
const warnings: unknown[][] = [];
|
||||||
|
const originalWarn = console.warn;
|
||||||
|
console.warn = (...args: unknown[]) => warnings.push(args);
|
||||||
|
try {
|
||||||
|
(0, eval)(getReactiveRuntime(true));
|
||||||
|
(
|
||||||
|
win as unknown as { __wrnexusHydrateScopes?: (root: unknown) => void }
|
||||||
|
).__wrnexusHydrateScopes?.(win.document);
|
||||||
|
} finally {
|
||||||
|
console.warn = originalWarn;
|
||||||
|
}
|
||||||
|
expect(warnings).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
test("production runtime strips development diagnostics", () => {
|
test("production runtime strips development diagnostics", () => {
|
||||||
const production = getReactiveRuntime();
|
const production = getReactiveRuntime();
|
||||||
expect(production).not.toContain("WRN-DEV-");
|
expect(production).not.toContain("WRN-DEV-");
|
||||||
@@ -1781,3 +1836,57 @@ test("a class binding inside data-for follows state the row never mentions", ()
|
|||||||
expect(items()[0]?.classList.contains("is-active")).toBe(false);
|
expect(items()[0]?.classList.contains("is-active")).toBe(false);
|
||||||
expect(items()[1]?.classList.contains("is-active")).toBe(true);
|
expect(items()[1]?.classList.contains("is-active")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a @focus binding on a component tag fires for a focusable element inside it", () => {
|
||||||
|
/*
|
||||||
|
* focus and blur do NOT bubble. The runtime bound every output-named DOM
|
||||||
|
* fallback listener in the bubble phase, so a parent writing @focus on a
|
||||||
|
* component tag never heard the component's own input or button take focus:
|
||||||
|
* the event fired on the descendant and stopped there. Nothing errored --
|
||||||
|
* the binding simply did nothing, which is why the library still declares
|
||||||
|
* focus/blur outputs on components that could never deliver them
|
||||||
|
* (button, TextLink, WysiwygEditor).
|
||||||
|
*
|
||||||
|
* The ui ratchet for "outputs nothing emits" excludes natively-named
|
||||||
|
* outputs on the stated grounds that a native event reaches the root
|
||||||
|
* anyway. That reasoning holds for click and change, which bubble, and
|
||||||
|
* fails for focus and blur, which do not.
|
||||||
|
*/
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="seen: ''">` +
|
||||||
|
`<span id="out">{seen}</span>` +
|
||||||
|
`<div data-scope="n: 0" data-wrn-hydration="Field:x">` +
|
||||||
|
`<div data-wrn-events="focus" data-wrn-out-focus="seen = 'focused'">` +
|
||||||
|
`<button id="inner">press</button>` +
|
||||||
|
`</div></div></div>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inner = win.document.querySelector("#inner") as unknown as {
|
||||||
|
dispatchEvent: (event: unknown) => boolean;
|
||||||
|
};
|
||||||
|
// A real focus event: fires on the descendant, does not bubble.
|
||||||
|
const FocusEventCtor = (win as unknown as { Event: typeof Event }).Event;
|
||||||
|
inner.dispatchEvent(new FocusEventCtor("focus", { bubbles: false }));
|
||||||
|
|
||||||
|
expect(win.document.querySelector("#out")?.textContent).toBe("focused");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a @change binding still fires for a bubbling event inside a component", () => {
|
||||||
|
// The companion case: change DOES bubble, and must keep working unchanged.
|
||||||
|
const win = mount(
|
||||||
|
`<div data-scope="seen: ''">` +
|
||||||
|
`<span id="out">{seen}</span>` +
|
||||||
|
`<div data-scope="n: 0" data-wrn-hydration="Field:y">` +
|
||||||
|
`<div data-wrn-events="change" data-wrn-out-change="seen = 'changed'">` +
|
||||||
|
`<input id="inner" />` +
|
||||||
|
`</div></div></div>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inner = win.document.querySelector("#inner") as unknown as {
|
||||||
|
dispatchEvent: (event: unknown) => boolean;
|
||||||
|
};
|
||||||
|
const EventCtor = (win as unknown as { Event: typeof Event }).Event;
|
||||||
|
inner.dispatchEvent(new EventCtor("change", { bubbles: true }));
|
||||||
|
|
||||||
|
expect(win.document.querySelector("#out")?.textContent).toBe("changed");
|
||||||
|
});
|
||||||
|
|||||||
@@ -297,3 +297,16 @@ const users = await usersRepo.all({
|
|||||||
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.
|
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses `$1`, `$2`, and SQLite/MySQL use `?`. List limits are bounded.
|
||||||
|
|
||||||
`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.
|
`retryTransaction()` retries recognized serialization, deadlock, and database-lock errors by default. Supply `shouldRetry` for application-specific retryable errors; ordinary validation or business errors are not retried automatically.
|
||||||
|
|
||||||
|
## Reusable seed helpers
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { addSeedData, removeSeedData, runSeedQuery, getDb } from "@wrnexus/db";
|
||||||
|
|
||||||
|
await addSeedData(getDb, [{ code: "free", credits: 100 }], "plans", { conflict: "ignore" });
|
||||||
|
await removeSeedData(getDb(), { code: "legacy" }, "plans");
|
||||||
|
await runSeedQuery(getDb(), "UPDATE plans SET credits = ? WHERE code = ?", [200, "free"]);
|
||||||
|
```
|
||||||
|
|
||||||
|
Table and column identifiers are validated, values are always parameterized, and empty bulk
|
||||||
|
deletes are refused unless `{ all: true }` is explicit.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/db",
|
"name": "@wrnexus/db",
|
||||||
"version": "0.8.16",
|
"version": "0.8.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"main": "./src/index.ts",
|
||||||
|
|||||||
@@ -29,8 +29,17 @@ export interface ModelRef {
|
|||||||
model: Model;
|
model: Model;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse annotated queries from one `.sql` file's contents. */
|
/**
|
||||||
|
* Parse annotated queries from one `.sql` file's contents.
|
||||||
|
*
|
||||||
|
* Line endings are normalised to LF first. The generator embeds each query's
|
||||||
|
* SQL as a string literal, so without this a CRLF checkout regenerated every
|
||||||
|
* query with CRLF where the committed file had LF -- a build dirtied the
|
||||||
|
* working tree, and that churn buried real changes in the same file. Line
|
||||||
|
* endings carry no meaning in SQL.
|
||||||
|
*/
|
||||||
export function parseQueries(content: string): QueryDef[] {
|
export function parseQueries(content: string): QueryDef[] {
|
||||||
|
content = content.replace(/\r\n?/g, "\n");
|
||||||
const out: QueryDef[] = [];
|
const out: QueryDef[] = [];
|
||||||
const re = /--\s*name:\s*(\w+)\s*:(one|many|exec)\b[^\n]*\n([\s\S]*?)(?=--\s*name:|$)/gi;
|
const re = /--\s*name:\s*(\w+)\s*:(one|many|exec)\b[^\n]*\n([\s\S]*?)(?=--\s*name:|$)/gi;
|
||||||
let m: RegExpExecArray | null;
|
let m: RegExpExecArray | null;
|
||||||
|
|||||||
@@ -63,6 +63,34 @@ export function withTransaction<T>(db: Db, callback: (tx: Db) => Promise<T>): Pr
|
|||||||
return db.tx(callback);
|
return db.tx(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Execute a parameterized compare-and-set update and report whether this caller won. */
|
||||||
|
export async function compareAndSet(
|
||||||
|
db: Db,
|
||||||
|
options: {
|
||||||
|
table: string;
|
||||||
|
idColumn?: string;
|
||||||
|
id: string | number;
|
||||||
|
stateColumn?: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
extra?: Record<string, unknown>;
|
||||||
|
},
|
||||||
|
): Promise<boolean> {
|
||||||
|
const table = identifier(options.table);
|
||||||
|
const idColumn = identifier(options.idColumn ?? "id");
|
||||||
|
const stateColumn = identifier(options.stateColumn ?? "status");
|
||||||
|
const entries = Object.entries(options.extra ?? {});
|
||||||
|
const assignments = [stateColumn, ...entries.map(([name]) => identifier(name))];
|
||||||
|
const dialect = db.driver.dialect;
|
||||||
|
const values = [options.to, ...entries.map(([, value]) => value), options.id, options.from];
|
||||||
|
const sql = `UPDATE ${table} SET ${assignments
|
||||||
|
.map((name, index) => `${name} = ${placeholder(dialect, index + 1)}`)
|
||||||
|
.join(
|
||||||
|
", ",
|
||||||
|
)} WHERE ${idColumn} = ${placeholder(dialect, assignments.length + 1)} AND ${stateColumn} = ${placeholder(dialect, assignments.length + 2)}`;
|
||||||
|
return (await db.exec(sql, values)).changes === 1;
|
||||||
|
}
|
||||||
|
|
||||||
/** Conservative default classifier for deadlock/serialization retry errors. */
|
/** Conservative default classifier for deadlock/serialization retry errors. */
|
||||||
export function isRetryableTransactionError(error: unknown): boolean {
|
export function isRetryableTransactionError(error: unknown): boolean {
|
||||||
if (!error || typeof error !== "object") return false;
|
if (!error || typeof error !== "object") return false;
|
||||||
|
|||||||
@@ -54,9 +54,29 @@ export {
|
|||||||
exists,
|
exists,
|
||||||
countRows,
|
countRows,
|
||||||
withTransaction,
|
withTransaction,
|
||||||
|
compareAndSet,
|
||||||
retryTransaction,
|
retryTransaction,
|
||||||
batch,
|
batch,
|
||||||
databaseHealth,
|
databaseHealth,
|
||||||
createRepository,
|
createRepository,
|
||||||
} from "./helpers.ts";
|
} from "./helpers.ts";
|
||||||
export type { Repository } from "./helpers.ts";
|
export type { Repository } from "./helpers.ts";
|
||||||
|
export {
|
||||||
|
addSeedData,
|
||||||
|
defineSeed,
|
||||||
|
removeSeedData,
|
||||||
|
runSeedQuery,
|
||||||
|
seedIfMissing,
|
||||||
|
seedUsers,
|
||||||
|
upsertSeedData,
|
||||||
|
} from "./seed.ts";
|
||||||
|
export type {
|
||||||
|
AddSeedOptions,
|
||||||
|
SeedDatabase,
|
||||||
|
SeedRow,
|
||||||
|
SeedStep,
|
||||||
|
SeedUserAccount,
|
||||||
|
UpsertSeedOptions,
|
||||||
|
} from "./seed.ts";
|
||||||
|
export { defineLedger } from "./ledger.ts";
|
||||||
|
export type { LedgerOptions } from "./ledger.ts";
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import type { Db } from "./driver.ts";
|
||||||
|
|
||||||
|
const SAFE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
||||||
|
const ident = (value: string) => {
|
||||||
|
if (!SAFE.test(value)) throw new TypeError(`Unsafe ledger identifier: ${value}`);
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface LedgerOptions {
|
||||||
|
db: () => Db;
|
||||||
|
table: string;
|
||||||
|
subjectColumn?: string;
|
||||||
|
amountColumn?: string;
|
||||||
|
kindColumn?: string;
|
||||||
|
reasonColumn?: string;
|
||||||
|
referenceColumn?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defineLedger(options: LedgerOptions) {
|
||||||
|
const table = ident(options.table);
|
||||||
|
const subject = ident(options.subjectColumn ?? "subject_id");
|
||||||
|
const amount = ident(options.amountColumn ?? "amount");
|
||||||
|
const kind = ident(options.kindColumn ?? "kind");
|
||||||
|
const reason = ident(options.reasonColumn ?? "reason");
|
||||||
|
const reference = ident(options.referenceColumn ?? "ref");
|
||||||
|
const positive = (value: number) => {
|
||||||
|
if (!Number.isSafeInteger(value) || value < 1)
|
||||||
|
throw new RangeError("ledger amount must be a positive safe integer");
|
||||||
|
};
|
||||||
|
const write = async (
|
||||||
|
subjectId: string,
|
||||||
|
delta: number,
|
||||||
|
entryKind: string,
|
||||||
|
entryReason: string,
|
||||||
|
ref = "",
|
||||||
|
) => {
|
||||||
|
if (!subjectId.trim()) throw new TypeError("ledger subject cannot be empty");
|
||||||
|
await options
|
||||||
|
.db()
|
||||||
|
.exec(
|
||||||
|
`INSERT INTO ${table} (${subject},${amount},${kind},${reason},${reference}) VALUES (?,?,?,?,?)`,
|
||||||
|
[subjectId, delta, entryKind, entryReason, ref],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const balance = async (subjectId: string, db = options.db()) => {
|
||||||
|
const row = await db.one<{ balance: number | string | null }>(
|
||||||
|
`SELECT COALESCE(SUM(${amount}),0) AS balance FROM ${table} WHERE ${subject} = ?`,
|
||||||
|
[subjectId],
|
||||||
|
);
|
||||||
|
return Number(row?.balance ?? 0);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
balance,
|
||||||
|
async credit(subjectId: string, value: number, entryReason: string, ref = "") {
|
||||||
|
positive(value);
|
||||||
|
await write(subjectId, value, "credit", entryReason, ref);
|
||||||
|
},
|
||||||
|
async debit(subjectId: string, value: number, entryReason: string, ref = "") {
|
||||||
|
positive(value);
|
||||||
|
return options.db().tx(async (db) => {
|
||||||
|
if ((await balance(subjectId, db)) < value) return false;
|
||||||
|
await db.exec(
|
||||||
|
`INSERT INTO ${table} (${subject},${amount},${kind},${reason},${reference}) VALUES (?,?,?,?,?)`,
|
||||||
|
[subjectId, -value, "debit", entryReason, ref],
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
refund: (subjectId: string, value: number, entryReason: string, ref = "") => {
|
||||||
|
positive(value);
|
||||||
|
return write(subjectId, value, "refund", entryReason, ref);
|
||||||
|
},
|
||||||
|
entries: (subjectId: string) =>
|
||||||
|
options.db().all(`SELECT * FROM ${table} WHERE ${subject} = ? ORDER BY id DESC`, [subjectId]),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import type { Db, ExecResult } from "./driver.ts";
|
||||||
|
|
||||||
|
export type SeedRow = Record<string, unknown>;
|
||||||
|
export type SeedDatabase = Db | (() => Db);
|
||||||
|
|
||||||
|
export interface AddSeedOptions {
|
||||||
|
conflict?: "error" | "ignore" | "replace";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpsertSeedOptions {
|
||||||
|
key: string | readonly string[];
|
||||||
|
update?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function identifier(value: string, label: string): string {
|
||||||
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
||||||
|
throw new TypeError(`${label} must be a safe SQL identifier`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function database(value: SeedDatabase): Db {
|
||||||
|
return typeof value === "function" ? value() : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insert one or many object rows with parameterized values. */
|
||||||
|
export async function addSeedData(
|
||||||
|
source: SeedDatabase,
|
||||||
|
data: SeedRow | readonly SeedRow[],
|
||||||
|
table: string,
|
||||||
|
options: AddSeedOptions = {},
|
||||||
|
): Promise<ExecResult[]> {
|
||||||
|
const db = database(source);
|
||||||
|
const target = identifier(table, "seed table");
|
||||||
|
const rows = Array.isArray(data) ? data : [data];
|
||||||
|
const results: ExecResult[] = [];
|
||||||
|
const clause =
|
||||||
|
options.conflict === "ignore"
|
||||||
|
? " OR IGNORE"
|
||||||
|
: options.conflict === "replace"
|
||||||
|
? " OR REPLACE"
|
||||||
|
: "";
|
||||||
|
for (const row of rows) {
|
||||||
|
const entries = Object.entries(row);
|
||||||
|
if (!entries.length) throw new TypeError("seed row cannot be empty");
|
||||||
|
const columns = entries.map(([column]) => identifier(column, "seed column"));
|
||||||
|
results.push(
|
||||||
|
await db.exec(
|
||||||
|
`INSERT${clause} INTO ${target} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`,
|
||||||
|
entries.map(([, value]) => value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Delete matching seed rows. An empty match is refused unless `all` is explicit. */
|
||||||
|
export async function removeSeedData(
|
||||||
|
source: SeedDatabase,
|
||||||
|
match: SeedRow,
|
||||||
|
table: string,
|
||||||
|
options: { all?: boolean } = {},
|
||||||
|
): Promise<ExecResult> {
|
||||||
|
const db = database(source);
|
||||||
|
const target = identifier(table, "seed table");
|
||||||
|
const entries = Object.entries(match);
|
||||||
|
if (!entries.length && !options.all) {
|
||||||
|
throw new TypeError("removeSeedData requires match fields or { all: true }");
|
||||||
|
}
|
||||||
|
const where = entries.length
|
||||||
|
? ` WHERE ${entries.map(([column]) => `${identifier(column, "seed column")} = ?`).join(" AND ")}`
|
||||||
|
: "";
|
||||||
|
return db.exec(
|
||||||
|
`DELETE FROM ${target}${where}`,
|
||||||
|
entries.map(([, value]) => value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Execute an application-owned parameterized seed statement. */
|
||||||
|
export function runSeedQuery(
|
||||||
|
source: SeedDatabase,
|
||||||
|
query: string,
|
||||||
|
data: readonly unknown[] = [],
|
||||||
|
): Promise<ExecResult> {
|
||||||
|
const db = database(source);
|
||||||
|
if (!query.trim()) throw new TypeError("seed query cannot be empty");
|
||||||
|
return db.exec(query, [...data]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Portable object upsert for SQLite, PostgreSQL and MySQL. */
|
||||||
|
export async function upsertSeedData(
|
||||||
|
source: SeedDatabase,
|
||||||
|
data: SeedRow | readonly SeedRow[],
|
||||||
|
table: string,
|
||||||
|
options: UpsertSeedOptions,
|
||||||
|
): Promise<ExecResult[]> {
|
||||||
|
const db = database(source);
|
||||||
|
const target = identifier(table, "seed table");
|
||||||
|
const keys = (Array.isArray(options.key) ? options.key : [options.key]).map((key) =>
|
||||||
|
identifier(key, "seed key"),
|
||||||
|
);
|
||||||
|
if (!keys.length) throw new TypeError("upsertSeedData requires at least one key");
|
||||||
|
const results: ExecResult[] = [];
|
||||||
|
for (const row of Array.isArray(data) ? data : [data]) {
|
||||||
|
const entries = Object.entries(row);
|
||||||
|
if (!entries.length) throw new TypeError("seed row cannot be empty");
|
||||||
|
const columns = entries.map(([column]) => identifier(column, "seed column"));
|
||||||
|
for (const key of keys)
|
||||||
|
if (!columns.includes(key)) throw new TypeError(`seed row is missing key '${key}'`);
|
||||||
|
const updates = (options.update ?? columns.filter((column) => !keys.includes(column))).map(
|
||||||
|
(column) => identifier(column, "seed update column"),
|
||||||
|
);
|
||||||
|
const prefix = db.driver.dialect === "mysql" ? "INSERT INTO" : "INSERT INTO";
|
||||||
|
const suffix =
|
||||||
|
db.driver.dialect === "mysql"
|
||||||
|
? ` ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(", ")}`
|
||||||
|
: ` ON CONFLICT (${keys.join(", ")}) DO ${
|
||||||
|
updates.length
|
||||||
|
? `UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(", ")}`
|
||||||
|
: "NOTHING"
|
||||||
|
}`;
|
||||||
|
results.push(
|
||||||
|
await db.exec(
|
||||||
|
`${prefix} ${target} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})${suffix}`,
|
||||||
|
entries.map(([, value]) => value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedIfMissing(
|
||||||
|
source: SeedDatabase,
|
||||||
|
table: string,
|
||||||
|
match: SeedRow,
|
||||||
|
data: SeedRow,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const db = database(source);
|
||||||
|
const target = identifier(table, "seed table");
|
||||||
|
const where = Object.entries(match);
|
||||||
|
if (!where.length) throw new TypeError("seedIfMissing requires match fields");
|
||||||
|
const existing = await db.one(
|
||||||
|
`SELECT 1 AS found FROM ${target} WHERE ${where
|
||||||
|
.map(([column]) => `${identifier(column, "seed column")} = ?`)
|
||||||
|
.join(" AND ")} LIMIT 1`,
|
||||||
|
where.map(([, value]) => value),
|
||||||
|
);
|
||||||
|
if (existing) return false;
|
||||||
|
await addSeedData(db, data, target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SeedStep = (db: Db) => void | Promise<void>;
|
||||||
|
|
||||||
|
export function defineSeed(...steps: readonly SeedStep[]): SeedStep {
|
||||||
|
return async (db) =>
|
||||||
|
db.tx(async (transaction) => {
|
||||||
|
for (const step of steps) await step(transaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SeedUserAccount {
|
||||||
|
identifier: string;
|
||||||
|
password?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function seedUsers<T extends SeedUserAccount, U extends { id: string }>(
|
||||||
|
accounts: readonly T[],
|
||||||
|
options: {
|
||||||
|
find(identifier: string): Promise<U | null>;
|
||||||
|
create(account: T): Promise<U>;
|
||||||
|
configure?(user: U, account: T): void | Promise<void>;
|
||||||
|
},
|
||||||
|
): Promise<U[]> {
|
||||||
|
const users: U[] = [];
|
||||||
|
for (const account of accounts) {
|
||||||
|
const user = (await options.find(account.identifier)) ?? (await options.create(account));
|
||||||
|
await options.configure?.(user, account);
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user