Compare commits
65
Commits
4be4b2c346
...
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 |
@@ -19,7 +19,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.3.14
|
||||
bun-version: 1.4.0
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/bun": "^1.4.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"eslint": "^10.8.1",
|
||||
@@ -133,7 +133,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@wrnexus/cli": "^0.8.35",
|
||||
"@wrnexus/cli": "^0.8.36",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^6.0.3",
|
||||
@@ -145,27 +145,27 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@wrnexus/ai": "^0.8.9",
|
||||
"@wrnexus/auth": "^0.8.11",
|
||||
"@wrnexus/auth": "^0.8.12",
|
||||
"@wrnexus/authz": "^0.8.9",
|
||||
"@wrnexus/captcha": "^0.8.10",
|
||||
"@wrnexus/captcha": "^0.8.11",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/csr": "^0.8.21",
|
||||
"@wrnexus/db": "^0.8.14",
|
||||
"@wrnexus/dev-server": "^0.8.32",
|
||||
"@wrnexus/encryption": "^0.8.8",
|
||||
"@wrnexus/db": "^0.8.15",
|
||||
"@wrnexus/dev-server": "^0.8.33",
|
||||
"@wrnexus/encryption": "^0.8.9",
|
||||
"@wrnexus/helpers": "^0.8.8",
|
||||
"@wrnexus/i18n": "^0.8.10",
|
||||
"@wrnexus/image": "^0.8.9",
|
||||
"@wrnexus/jwt": "^0.8.8",
|
||||
"@wrnexus/i18n": "^0.8.11",
|
||||
"@wrnexus/image": "^0.8.10",
|
||||
"@wrnexus/jwt": "^0.8.9",
|
||||
"@wrnexus/observability": "^0.8.8",
|
||||
"@wrnexus/realtime": "^0.8.9",
|
||||
"@wrnexus/realtime": "^0.8.10",
|
||||
"@wrnexus/security": "^0.8.8",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/styles": "^0.8.15",
|
||||
"@wrnexus/tracking": "^0.8.8",
|
||||
"@wrnexus/ui": "^0.8.19",
|
||||
"@wrnexus/uploader": "^0.8.9",
|
||||
"@wrnexus/validation": "^0.8.9",
|
||||
"@wrnexus/uploader": "^0.8.10",
|
||||
"@wrnexus/validation": "^0.8.10",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -173,7 +173,7 @@
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@wrnexus/cli": "^0.8.35",
|
||||
"@wrnexus/cli": "^0.8.36",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"tailwindcss": "^4.3.3",
|
||||
@@ -186,27 +186,27 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@wrnexus/ai": "^0.8.9",
|
||||
"@wrnexus/auth": "^0.8.11",
|
||||
"@wrnexus/auth": "^0.8.12",
|
||||
"@wrnexus/authz": "^0.8.9",
|
||||
"@wrnexus/captcha": "^0.8.10",
|
||||
"@wrnexus/captcha": "^0.8.11",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/csr": "^0.8.21",
|
||||
"@wrnexus/db": "^0.8.14",
|
||||
"@wrnexus/dev-server": "^0.8.32",
|
||||
"@wrnexus/encryption": "^0.8.8",
|
||||
"@wrnexus/db": "^0.8.15",
|
||||
"@wrnexus/dev-server": "^0.8.33",
|
||||
"@wrnexus/encryption": "^0.8.9",
|
||||
"@wrnexus/helpers": "^0.8.8",
|
||||
"@wrnexus/i18n": "^0.8.10",
|
||||
"@wrnexus/image": "^0.8.9",
|
||||
"@wrnexus/jwt": "^0.8.8",
|
||||
"@wrnexus/i18n": "^0.8.11",
|
||||
"@wrnexus/image": "^0.8.10",
|
||||
"@wrnexus/jwt": "^0.8.9",
|
||||
"@wrnexus/observability": "^0.8.8",
|
||||
"@wrnexus/realtime": "^0.8.9",
|
||||
"@wrnexus/realtime": "^0.8.10",
|
||||
"@wrnexus/security": "^0.8.8",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/styles": "^0.8.15",
|
||||
"@wrnexus/tracking": "^0.8.8",
|
||||
"@wrnexus/ui": "^0.8.19",
|
||||
"@wrnexus/uploader": "^0.8.9",
|
||||
"@wrnexus/validation": "^0.8.9",
|
||||
"@wrnexus/uploader": "^0.8.10",
|
||||
"@wrnexus/validation": "^0.8.10",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -214,7 +214,7 @@
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@wrnexus/cli": "^0.8.35",
|
||||
"@wrnexus/cli": "^0.8.36",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"tailwindcss": "^4.3.3",
|
||||
@@ -257,7 +257,7 @@
|
||||
},
|
||||
"packages/authz": {
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.8.14",
|
||||
"version": "0.8.15",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/db": "workspace:*",
|
||||
@@ -277,7 +277,7 @@
|
||||
},
|
||||
"packages/captcha": {
|
||||
"name": "@wrnexus/captcha",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.14",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.57",
|
||||
"version": "0.8.64",
|
||||
"bin": {
|
||||
"wrnexus": "src/index.ts",
|
||||
},
|
||||
@@ -320,7 +320,7 @@
|
||||
},
|
||||
"packages/compiler": {
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.19",
|
||||
"version": "0.8.22",
|
||||
"dependencies": {
|
||||
"@wrnexus/csr": "workspace:*",
|
||||
"@wrnexus/store": "workspace:*",
|
||||
@@ -342,7 +342,7 @@
|
||||
},
|
||||
"packages/csr": {
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.32",
|
||||
"version": "0.8.35",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
@@ -357,7 +357,7 @@
|
||||
},
|
||||
"packages/dev-server": {
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.50",
|
||||
"version": "0.8.56",
|
||||
"dependencies": {
|
||||
"@wrnexus/authz": "workspace:*",
|
||||
"@wrnexus/cache": "workspace:*",
|
||||
@@ -372,6 +372,7 @@
|
||||
"@wrnexus/pubsub": "workspace:*",
|
||||
"@wrnexus/pwa": "workspace:*",
|
||||
"@wrnexus/queue": "workspace:*",
|
||||
"@wrnexus/react": "workspace:*",
|
||||
"@wrnexus/router": "workspace:*",
|
||||
"@wrnexus/rpc": "workspace:*",
|
||||
"@wrnexus/security": "workspace:*",
|
||||
@@ -385,7 +386,7 @@
|
||||
},
|
||||
"packages/dev-toolbar": {
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.16",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^6.0.3",
|
||||
@@ -487,8 +488,9 @@
|
||||
},
|
||||
"packages/mail": {
|
||||
"name": "@wrnexus/mail",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.3",
|
||||
"dependencies": {
|
||||
"@wrnexus/encryption": "workspace:*",
|
||||
"@wrnexus/queue": "workspace:*",
|
||||
},
|
||||
},
|
||||
@@ -528,6 +530,14 @@
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/payment": {
|
||||
"name": "@wrnexus/payment",
|
||||
"version": "0.8.1",
|
||||
"dependencies": {
|
||||
"@wrnexus/db": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/playground": {
|
||||
"name": "@wrnexus/playground",
|
||||
"version": "0.8.8",
|
||||
@@ -561,7 +571,7 @@
|
||||
},
|
||||
"packages/queue": {
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.14",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/db": "workspace:*",
|
||||
@@ -645,11 +655,11 @@
|
||||
},
|
||||
"packages/store": {
|
||||
"name": "@wrnexus/store",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.12",
|
||||
},
|
||||
"packages/styles": {
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.8.19",
|
||||
"version": "0.8.20",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
@@ -658,11 +668,11 @@
|
||||
},
|
||||
"packages/syntax": {
|
||||
"name": "@wrnexus/syntax",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.18",
|
||||
},
|
||||
"packages/test": {
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.12",
|
||||
},
|
||||
"packages/tracking": {
|
||||
"name": "@wrnexus/tracking",
|
||||
@@ -670,7 +680,7 @@
|
||||
},
|
||||
"packages/typecheck": {
|
||||
"name": "@wrnexus/typecheck",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.16",
|
||||
"dependencies": {
|
||||
"@wrnexus/syntax": "workspace:*",
|
||||
"typescript": "^6.0.3",
|
||||
@@ -678,14 +688,14 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@wrnexus/ui",
|
||||
"version": "0.8.23",
|
||||
"version": "0.8.56",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
},
|
||||
},
|
||||
"packages/uploader": {
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.13",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
@@ -786,7 +796,7 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -812,7 +822,7 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -832,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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
@@ -862,79 +874,79 @@
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
@@ -966,7 +978,7 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -974,11 +986,11 @@
|
||||
|
||||
"@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-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=="],
|
||||
|
||||
@@ -1066,6 +1078,8 @@
|
||||
|
||||
"@wrnexus/observability": ["@wrnexus/observability@workspace:packages/observability"],
|
||||
|
||||
"@wrnexus/payment": ["@wrnexus/payment@workspace:packages/payment"],
|
||||
|
||||
"@wrnexus/playground": ["@wrnexus/playground@workspace:packages/playground"],
|
||||
|
||||
"@wrnexus/plugin": ["@wrnexus/plugin@workspace:packages/plugin"],
|
||||
@@ -1108,7 +1122,7 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -1134,7 +1148,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1166,7 +1180,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1188,7 +1202,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1224,7 +1238,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1232,7 +1246,7 @@
|
||||
|
||||
"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"],
|
||||
|
||||
@@ -1304,7 +1318,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1340,7 +1354,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1358,7 +1372,7 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1370,9 +1384,9 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
@@ -1448,27 +1462,27 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
|
||||
+182
-2
@@ -968,6 +968,7 @@
|
||||
},
|
||||
"@wrnexus/cli": {
|
||||
".": [],
|
||||
"./test-setup": [],
|
||||
"./workspace": [
|
||||
"ResolvedWorkspaceApp",
|
||||
"ResolvedWorkspaceConfig",
|
||||
@@ -1352,6 +1353,8 @@
|
||||
"ActionClientError",
|
||||
"ActionClientOptions",
|
||||
"ActionResult",
|
||||
"ApiClientOptions",
|
||||
"ApiRequest",
|
||||
"ClientModuleScope",
|
||||
"HydrationScopeApi",
|
||||
"NAV_RUNTIME",
|
||||
@@ -1365,6 +1368,7 @@
|
||||
"callServerFunction",
|
||||
"collectRefs",
|
||||
"createActionClient",
|
||||
"createApiClient",
|
||||
"createOutputProxy",
|
||||
"createServerProxy",
|
||||
"getActionRuntime",
|
||||
@@ -1375,11 +1379,13 @@
|
||||
"invalidateClientModule",
|
||||
"invokeOutput",
|
||||
"loadClientFunctions",
|
||||
"registerOutputHandler"
|
||||
"registerOutputHandler",
|
||||
"useFetch"
|
||||
]
|
||||
},
|
||||
"@wrnexus/db": {
|
||||
".": [
|
||||
"AddSeedOptions",
|
||||
"BaseType",
|
||||
"Column",
|
||||
"ColumnDef",
|
||||
@@ -1390,6 +1396,7 @@
|
||||
"Dialect",
|
||||
"Driver",
|
||||
"ExecResult",
|
||||
"LedgerOptions",
|
||||
"Migration",
|
||||
"MigrationRunOptions",
|
||||
"MigrationSafetyIssue",
|
||||
@@ -1406,7 +1413,13 @@
|
||||
"RelationOptions",
|
||||
"Repository",
|
||||
"Row",
|
||||
"SeedDatabase",
|
||||
"SeedRow",
|
||||
"SeedStep",
|
||||
"SeedUserAccount",
|
||||
"TxHandle",
|
||||
"UpsertSeedOptions",
|
||||
"addSeedData",
|
||||
"analyzeMigrationSafety",
|
||||
"analyzeMigrations",
|
||||
"appliedMigrations",
|
||||
@@ -1421,6 +1434,8 @@
|
||||
"cursorPaginate",
|
||||
"databaseHealth",
|
||||
"databaseNames",
|
||||
"defineLedger",
|
||||
"defineSeed",
|
||||
"exists",
|
||||
"firstOrThrow",
|
||||
"generateQueriesFile",
|
||||
@@ -1438,15 +1453,20 @@
|
||||
"queryOperation",
|
||||
"registerDb",
|
||||
"registerLazyDb",
|
||||
"removeSeedData",
|
||||
"resetDbPerformanceSnapshot",
|
||||
"retryTransaction",
|
||||
"rollback",
|
||||
"runSeedQuery",
|
||||
"scaffoldMigration",
|
||||
"seedIfMissing",
|
||||
"seedUsers",
|
||||
"setDb",
|
||||
"softDeleteClause",
|
||||
"status",
|
||||
"table",
|
||||
"tenantScope",
|
||||
"upsertSeedData",
|
||||
"v",
|
||||
"withTransaction"
|
||||
],
|
||||
@@ -1647,6 +1667,7 @@
|
||||
"EncryptionKey",
|
||||
"EncryptionKeyring",
|
||||
"ReplayStore",
|
||||
"SecretResourceOptions",
|
||||
"createEncryptedRequest",
|
||||
"createKeyring",
|
||||
"createMemoryReplayStore",
|
||||
@@ -1654,6 +1675,7 @@
|
||||
"decryptEncryptedResponse",
|
||||
"decryptHttpBody",
|
||||
"decryptRequest",
|
||||
"defineSecretResource",
|
||||
"deriveKey",
|
||||
"encrypt",
|
||||
"encryptHttpBody",
|
||||
@@ -1687,26 +1709,39 @@
|
||||
"@wrnexus/helpers": {
|
||||
".": [
|
||||
"AllowedHosts",
|
||||
"ApiHandler",
|
||||
"HttpError",
|
||||
"LoginRedirectOptions",
|
||||
"OriginalRequestOptions",
|
||||
"RequestContext",
|
||||
"ResourceDefinition",
|
||||
"ResourceHandlers",
|
||||
"RetryOptions",
|
||||
"appOrigin",
|
||||
"appUrl",
|
||||
"authorized",
|
||||
"backoffDelay",
|
||||
"clamp",
|
||||
"currentAppName",
|
||||
"currentAppOrigin",
|
||||
"defineApiRoute",
|
||||
"defineResource",
|
||||
"getOriginalRequestMethod",
|
||||
"getOriginalRequestOrigin",
|
||||
"getOriginalRequestPath",
|
||||
"getOriginalRequestUrl",
|
||||
"json",
|
||||
"once",
|
||||
"redirectToLogin",
|
||||
"requireJson",
|
||||
"requireParam",
|
||||
"requirePermission",
|
||||
"requireUser",
|
||||
"retry",
|
||||
"safeJsonParse",
|
||||
"sleep",
|
||||
"stableStringify",
|
||||
"subjectId",
|
||||
"withTimeout",
|
||||
"workspaceAppOrigins",
|
||||
"workspaceRootDomain"
|
||||
@@ -1902,6 +1937,25 @@
|
||||
"isApiReferenceAt"
|
||||
]
|
||||
},
|
||||
"@wrnexus/mail": {
|
||||
".": [
|
||||
"MailDriver",
|
||||
"MailMessage",
|
||||
"MailOptions",
|
||||
"MailSandboxDecision",
|
||||
"MailTemplate",
|
||||
"SealedMailCredentialsOptions",
|
||||
"SealedMailDriverOptions",
|
||||
"defineMail",
|
||||
"defineMailTemplate",
|
||||
"defineSealedMailCredentials",
|
||||
"interpolateTemplate",
|
||||
"mailSandboxDecision",
|
||||
"queuedMail",
|
||||
"sealedMailDriver",
|
||||
"storedMailTemplate"
|
||||
]
|
||||
},
|
||||
"@wrnexus/mcp": {
|
||||
".": [
|
||||
"McpServer",
|
||||
@@ -1913,6 +1967,20 @@
|
||||
"runMcpStdio"
|
||||
]
|
||||
},
|
||||
"@wrnexus/metering": {
|
||||
".": [
|
||||
"EntitlementPlan",
|
||||
"EntitlementsOptions",
|
||||
"MeterKind",
|
||||
"MeterOptions",
|
||||
"MeterPack",
|
||||
"MeterResult",
|
||||
"MeterStore",
|
||||
"defineEntitlements",
|
||||
"defineMeter",
|
||||
"definePacks"
|
||||
]
|
||||
},
|
||||
"@wrnexus/mobile": {
|
||||
".": [
|
||||
"CapacitorBridge",
|
||||
@@ -2113,6 +2181,70 @@
|
||||
"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": {
|
||||
".": [
|
||||
"PlaygroundCompilation",
|
||||
@@ -2277,21 +2409,41 @@
|
||||
"@wrnexus/queue": {
|
||||
".": [
|
||||
"AddOptions",
|
||||
"DefineQueueOptions",
|
||||
"DefinedJob",
|
||||
"DefinedJobOptions",
|
||||
"DefinedJobs",
|
||||
"DefinedQueue",
|
||||
"DurableQueue",
|
||||
"DurableQueueOptions",
|
||||
"Job",
|
||||
"JobContext",
|
||||
"JobDefinition",
|
||||
"JobHandler",
|
||||
"JobStateMachineOptions",
|
||||
"JobStatus",
|
||||
"OutboxEntry",
|
||||
"OutboxWriter",
|
||||
"POSTGRES_QUEUE_SCHEMA",
|
||||
"Queue",
|
||||
"QueueAdminOptions",
|
||||
"QueueDashboardSnapshot",
|
||||
"QueueEvent",
|
||||
"QueueEventType",
|
||||
"QueueHealth",
|
||||
"QueueJobDefinitions",
|
||||
"QueueJobRecord",
|
||||
"QueueJobState",
|
||||
"QueueOptions",
|
||||
"QueueScheduler",
|
||||
"QueueStorage",
|
||||
"QueueStorageConfig",
|
||||
"QueueStore",
|
||||
"RedisQueueClient",
|
||||
"SagaStep",
|
||||
"ScheduledJob",
|
||||
"SqlQueueClient",
|
||||
"SqliteQueueClient",
|
||||
"SubjectJob",
|
||||
"SubjectQueue",
|
||||
"WorkerDefinition",
|
||||
@@ -2303,24 +2455,46 @@
|
||||
"WorkflowStep",
|
||||
"WorkflowStore",
|
||||
"addBatch",
|
||||
"configureQueueStorage",
|
||||
"configuredQueueStore",
|
||||
"createDurableQueue",
|
||||
"createQueue",
|
||||
"createQueueAdminHandler",
|
||||
"createQueueScheduler",
|
||||
"createWorkflowEngine",
|
||||
"cronToInterval",
|
||||
"databaseQueueStore",
|
||||
"defineDurableWorkflow",
|
||||
"defineJob",
|
||||
"defineJobStateMachine",
|
||||
"defineQueue",
|
||||
"defineSaga",
|
||||
"defineWorker",
|
||||
"defineWorkflow",
|
||||
"drainOutbox",
|
||||
"installDatabaseQueueSchema",
|
||||
"installOutboxSchema",
|
||||
"installSqliteQueueSchema",
|
||||
"memoryQueueStore",
|
||||
"memoryWorkflowStore",
|
||||
"postgresQueueStore",
|
||||
"queueDashboardSnapshot",
|
||||
"queueStorageConfig",
|
||||
"redisQueueStore",
|
||||
"renderQueueDashboard",
|
||||
"runQueueDaemon",
|
||||
"runWorker",
|
||||
"subjectQueue"
|
||||
"sqliteQueueSchema",
|
||||
"sqliteQueueStore",
|
||||
"subjectQueue",
|
||||
"testQueue",
|
||||
"withOutbox"
|
||||
],
|
||||
"./sqlite": [
|
||||
"SqliteQueueClient",
|
||||
"installSqliteQueueSchema",
|
||||
"sqliteQueueSchema",
|
||||
"sqliteQueueStore"
|
||||
]
|
||||
},
|
||||
"@wrnexus/react": {
|
||||
@@ -2363,6 +2537,7 @@
|
||||
".": [
|
||||
"AnimationTimeline",
|
||||
"Cleanup",
|
||||
"FormState",
|
||||
"HistorySignal",
|
||||
"ReactiveContext",
|
||||
"ReactiveScope",
|
||||
@@ -2389,6 +2564,7 @@
|
||||
"transition",
|
||||
"untrack",
|
||||
"urlSignal",
|
||||
"useForm",
|
||||
"watch"
|
||||
]
|
||||
},
|
||||
@@ -2916,11 +3092,14 @@
|
||||
".": [
|
||||
"BrowserArtifactPage",
|
||||
"Deferred",
|
||||
"DetectMutationsOptions",
|
||||
"FetchMock",
|
||||
"Harness",
|
||||
"HarnessOptions",
|
||||
"JsonResponse",
|
||||
"MemoryCookieJar",
|
||||
"MutationCase",
|
||||
"MutationReport",
|
||||
"TestRequestOptions",
|
||||
"TransactionalDatabase",
|
||||
"WaitForOptions",
|
||||
@@ -2938,6 +3117,7 @@
|
||||
"createTestContext",
|
||||
"deferred",
|
||||
"describe",
|
||||
"detectMutations",
|
||||
"expect",
|
||||
"expectProblem",
|
||||
"it",
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
"artifacts": {
|
||||
"packages/ui/components/Accordion.wrn": "09251f76a2f9385e97eb67e06c0cf868e1771ac5f420647221bec5160287034b",
|
||||
"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/AuthSplitLayout.wrn": "724938921bc38cc5a0422de69c237b630f099f2df94c66977362b048e4a420a5",
|
||||
"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/Breadcrumb.wrn": "9f6c23550ebfc635c36ca8660953775170b4a471c0cd0206b9a7ba760994980a",
|
||||
"packages/ui/components/ButtonGroup.wrn": "fc23565ad56bd8483fcb777d9e02ccba2f9ea476c085754d42503bb83b718f72",
|
||||
"packages/ui/components/CTASection.wrn": "d0665f2aa33eea95e6f84120f739b3dd9c8723c882138bcb1a7f80dcd7909904",
|
||||
"packages/ui/components/Card.wrn": "3cc2ff238bda279c169026a462236144ad11ec0046deef00ab00e001a07e8a4c",
|
||||
"packages/ui/components/CTASection.wrn": "f6872b980432d5ee9d88388250be9838ccce51bd706349b283ef472ee692b237",
|
||||
"packages/ui/components/Card.wrn": "be8cd6a438d1ede4c284a302b76f0fbc235a2423b83a7ced21131763fecc8a90",
|
||||
"packages/ui/components/Carousel.wrn": "e7d921f802aa19210f3f415b59b70750a2c8d78e30684ad334809741c70a6bf9",
|
||||
"packages/ui/components/Chart.wrn": "07bc01247b5a1c2b82c2ba8386b7fcdbf480efead75d52b7da04d471072c7ee4",
|
||||
"packages/ui/components/ChatBubble.wrn": "d71f7d6567d76ac0eb3ecc8e2e567fec12bb3236ea05f4d0a72286abc0b7cbb1",
|
||||
"packages/ui/components/Clipboard.wrn": "5a93f2bae337d7ce364229b796bac673f60dde6dc135f0969f4e106e04854d30",
|
||||
"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/Combobox.wrn": "58488dbb60e41dacbf223afdb4a05abdbec53b7f54cd79a0db659add7057b1c1",
|
||||
"packages/ui/components/Confetti.wrn": "0e81426a3ee8aff672385a5466cf96085c25ba17677a2fef432bcddf13f4d58e",
|
||||
@@ -28,7 +28,7 @@
|
||||
"packages/ui/components/CustomScrollbar.wrn": "2a3a2719580c21823d079c92ebb000c7a5a7d9d16e3fa9275b8fb7d4449cb437",
|
||||
"packages/ui/components/DataMap.wrn": "338f0d0a3307289dd0387b7fe7ba631fa0b65b6be8c7b9ee3feb2203306f76a3",
|
||||
"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/Divider.wrn": "bfa36cf16ccd48c996bb0b08a438f1b281ea1811049ed69fc7680925b1eba019",
|
||||
"packages/ui/components/DragAndDrop.wrn": "84305f07d43cdb9eff1f63c19458d88b0aec013424d05a49a2cafbecedc60513",
|
||||
@@ -36,15 +36,15 @@
|
||||
"packages/ui/components/Dropdown.wrn": "978367a9dde22448cc664d686e4dd9c579fe8ba0b2cf02d610d95a3501f8dbb0",
|
||||
"packages/ui/components/FeatureCard.wrn": "1bc548c115f924a0c7ade4c557852757083755c60ca0ee747b150a7cca3ebced",
|
||||
"packages/ui/components/FeatureGrid.wrn": "15b5d6f9ddb7e3697682e7247fdf3c2dd080b770335f84ba9fab2692166fe897",
|
||||
"packages/ui/components/FeatureIconCard.wrn": "3b4f3fa62c6729e686886a5b848e26c255766684829cec2b715967b4e73d6f07",
|
||||
"packages/ui/components/FileInput.wrn": "866a292a3280527bf429893469e7c50f7cf38965d8adb37b45a35ede1606d5b8",
|
||||
"packages/ui/components/FileUploadProgress.wrn": "e5da29c562a521cdd6bb50b8f4d217aa1ab981d7b2a2432af47391472f0a8034",
|
||||
"packages/ui/components/Footer.wrn": "f7c5a77064a09e244f689d42475c4c20143f4c8859cac451c10a50c14652db33",
|
||||
"packages/ui/components/FeatureIconCard.wrn": "5f65d64235babc3e653a5695204412202015e6f3ffadca566fc18c7f9ce70428",
|
||||
"packages/ui/components/FileInput.wrn": "611047c362458a4e2513b741cc3ef43fbe54c10ddafc13775e4ff39c9de34931",
|
||||
"packages/ui/components/FileUploadProgress.wrn": "ba00557afebf60e6221385d6efcf08635789328deee6e551fc53fbb71e1f413b",
|
||||
"packages/ui/components/Footer.wrn": "e3515e05ef3ef51e6dad71d67ddfe110c80fdcf69e952aecdc7973e69d4f08c7",
|
||||
"packages/ui/components/Grid.wrn": "83d4f5f2656d539538723f5791ba3c238901432e9d8c55c74f068d3eb5a74517",
|
||||
"packages/ui/components/Hero.wrn": "345478b212701817ff57060f87982a987baf01a7179c979768e1cd4218b50900",
|
||||
"packages/ui/components/HeroActions.wrn": "67cd31400ccb6abdbbf16219267dee946b44b064b79f25276d20ceb7d6e8a790",
|
||||
"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/Kbd.wrn": "6b402cb93f0fa76bb978d1ba483215f70ec7214b1c5e84b568fa3e26e87ed9d9",
|
||||
"packages/ui/components/LayoutSplitter.wrn": "41d070e801a271552023e4392e4c0f67327c61e2e8eaee5f8b87af254b1d79c1",
|
||||
@@ -55,38 +55,38 @@
|
||||
"packages/ui/components/Map.wrn": "3e6d5de5a21dd050c5c65950d6fb2f89af4024e1856a48f055b65b88fcd7b890",
|
||||
"packages/ui/components/MarketingSectionHeader.wrn": "39b79499b77dc1c7d34e4e5ecb45a9ddc6182f78c2a2586671c4fa07f44679ad",
|
||||
"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/MetricGrid.wrn": "c71e53249835908833055b553e64a8ee55fdf11ac4605436a9581ebb87a0608b",
|
||||
"packages/ui/components/MetricGrid.wrn": "1fcebf51dcbe8520fe16bda904c1ef206c25c3900438d53877492c64ea21a1a5",
|
||||
"packages/ui/components/Modal.wrn": "7fa4b877772736f29f690e24d8742922b310397ef3083f0b78acb67a9f0d14b2",
|
||||
"packages/ui/components/Nav.wrn": "c45b0ecb42250f4b3ede33c8932a025f789dda9ef48dbf332459ae458163e69a",
|
||||
"packages/ui/components/Navbar.wrn": "01903230210cd2e5e6d9325a0708f85af9623d4118e3a53f1adb770fd545425d",
|
||||
"packages/ui/components/PageHeader.wrn": "321cde36ce8d521a57901033d46e8b437f42e558cca27f49ca26f7172aff1d54",
|
||||
"packages/ui/components/Navbar.wrn": "b68a824e479ce6957afc3e9710442281cdc1a8d02cd96501fe060271f54368e4",
|
||||
"packages/ui/components/PageHeader.wrn": "29a93cdca7285548b79ebe5b6421eef6dbb4d5ae8be2508fa2d681e1068ba07f",
|
||||
"packages/ui/components/Pagination.wrn": "d54226705d4556f76ee5f0d6ae82d101ca6d006397756d547a9aa5954415ba39",
|
||||
"packages/ui/components/PinInput.wrn": "4dc398456f6392d7925db941debb484c7cb0358ecef527f696fe5ec4800a7602",
|
||||
"packages/ui/components/Popover.wrn": "1246b99b4236bc7109a5d1427d013bb07bbea7a2da4f821517d2545ee0b5c019",
|
||||
"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/RangeSlider.wrn": "bbf646c5d5a89a62986290db811d77b8f635ebcaea9ca2d334b569aa5edc785b",
|
||||
"packages/ui/components/RangeSlider.wrn": "e8ab41782c9d6e58638f0bfccf6e3172af4ebf552189d2199cc6c96bd1652067",
|
||||
"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/Section.wrn": "45b7a1977c25a6020d5cc8bab2ce9c80730e7ec2d51ad53e8c9f64cd9b5b64cd",
|
||||
"packages/ui/components/Section.wrn": "78da13eb8bc1acafe8e6151cbaeed4baaac2ab860e3209f1d13c467f279d2e68",
|
||||
"packages/ui/components/SectionHeader.wrn": "aca2df8eb35806096f5b9af466b018e5c22a19422e8665f488380da577a0ceb5",
|
||||
"packages/ui/components/Sidebar.wrn": "e0250a973aea589a4d7dcc8b30dd01f22592bf22ba012e54a672b2a2288d8af6",
|
||||
"packages/ui/components/SplitHero.wrn": "843387dc7b920b3cf6a1b2b785b8653c281982e524479edc2638a89381ff3577",
|
||||
"packages/ui/components/StatsBar.wrn": "c273bd6ca417a0a20e5e6f1214414f678741b73bedd885fea1037575fa8c5b95",
|
||||
"packages/ui/components/SplitHero.wrn": "7127cb869d1228522d066e028818dd9e512de8b6dd8f99e3dec500e6da5a3cb0",
|
||||
"packages/ui/components/StatsBar.wrn": "6fabcb4374b1e074f58ebcaba968f235c8a3409762163aea51298e2515a14435",
|
||||
"packages/ui/components/Stepper.wrn": "7ec0902acd7cb74b44d490215b6421dae9a41731735ad1e9aec17e8bfc3fa142",
|
||||
"packages/ui/components/StrongPassword.wrn": "407eabcc13affc9323a346ddba8e39c81b885a59ef7043d2bd5567d4b7553282",
|
||||
"packages/ui/components/StrongPassword.wrn": "cc31cb080c4b4116c544b4340b8d740d08fec1b05563372b3280c50759416c87",
|
||||
"packages/ui/components/StyledIcon.wrn": "0874f9a234b053a7c8b9c6983be5d664453a64c4576dff1a049101e249a2866d",
|
||||
"packages/ui/components/Tabs.wrn": "f85214960e62da2fac41228deba511a50c57fd94f2830a8233b75048f6a542e9",
|
||||
"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/Toaster.wrn": "5e4c9b2c1bfd5675a81a32e57684b99393ac7fe45d8420c7409a4e95d47d080c",
|
||||
"packages/ui/components/ToggleCount.wrn": "e577195ee05ddb43cceec47145f6b90c0149f2f17055733d58911fcd02de2c7f",
|
||||
"packages/ui/components/TogglePassword.wrn": "6bd4d9fa82b0c8e80178430d5a42b1b3ec9725fb62be158585f68af9338c901a",
|
||||
"packages/ui/components/TogglePassword.wrn": "caa58479e5c8b1e3e504181a56a7d6bcf71d0a230010733710961c31e1cc58ab",
|
||||
"packages/ui/components/TreeView.wrn": "f54d8495730203b923629bcfb167d5ca41236af0101ea2723d33354413555b4b",
|
||||
"packages/ui/components/Typography.wrn": "9676af57f2a937b21f45512e0a290b5bee4a16267fbf404850297d29645d88b5",
|
||||
"packages/ui/components/WysiwygEditor.wrn": "a858ebd4e6e2672463ba57d4d049176ca1cc98dac32ab3df3638d6a9cdcd0f2d",
|
||||
@@ -94,15 +94,15 @@
|
||||
"packages/ui/components/avatar.wrn": "8bc705e459b13c7f063c57e1506ce540e50fa8ef9020ae85b6e8b6a3579fc5dc",
|
||||
"packages/ui/components/badge.wrn": "e44e33633fb34e897696cd9290f210108e35a3e2dc3b2a4a367411f45c69a1e1",
|
||||
"packages/ui/components/button.wrn": "65570c58cb2547f1e17cd158eec7cf7b1ffc721e6976c6486c9df564627b1219",
|
||||
"packages/ui/components/checkbox.wrn": "18046396b75d0b9c6bb2bdb09dbff1846c84fb07490f390a25ca7b5367fd2ef4",
|
||||
"packages/ui/components/input.wrn": "e4c14527f009b610b4aa96d8d7b7d5ac3ca1ffd34237bab348ee9dc4ddc55847",
|
||||
"packages/ui/components/checkbox.wrn": "da735354b57f33946591e1bd018b521ff709bae1325e2317735824673188377a",
|
||||
"packages/ui/components/input.wrn": "2f17353cbf0711ca50a935ae03ee9ee53594d7e35eb73f5df573fadb7958fac2",
|
||||
"packages/ui/components/progress.wrn": "6307a90585197d7aab19a8710b2430f5d4ed27ce77e9b90b1414ea0eed876492",
|
||||
"packages/ui/components/radio.wrn": "09425a78358de5bbd2f47482f313e80065135335b969ce5ccdd5c7cc3ea5232c",
|
||||
"packages/ui/components/select.wrn": "1d8d47a74d5ab9ed58d0ba11f46910897233b96652d17f5962f5dda60bd4cf8a",
|
||||
"packages/ui/components/radio.wrn": "df8e562324b6d99f5a157b0971d63bd715daa0cf6189e7b0d4df04d6f0411388",
|
||||
"packages/ui/components/select.wrn": "747321d6292b8052e44e8014e87ce25f4b56d8e07f86f1061e0ed0cec7651356",
|
||||
"packages/ui/components/skeleton.wrn": "4fc5e0846eeefd7830c038e1789be995c4f9d833aa913ff079eb4863baa65648",
|
||||
"packages/ui/components/spinner.wrn": "2322645da7ef53f7c06035ff071d9a2f6ffe2901ff9338daed84037369367105",
|
||||
"packages/ui/components/switch.wrn": "874504e4828e9db6a570d78984c4d3a76d0ceb39d70baa877649cb3798c82c85",
|
||||
"packages/ui/components/textarea.wrn": "9870d37664471a103d44435a3977bf40b087a743acd76ee4d77df9b56cccbb2b",
|
||||
"packages/ui/components/switch.wrn": "243f521431ffb644089df7f629bace0c07094638dfdd27e75b2017efa9cbba4f",
|
||||
"packages/ui/components/textarea.wrn": "aa3a75214e998a0b57c50e416476d6f85fa0313aaf97262f2f9222150c6c9e26",
|
||||
"packages/ui/components/tooltip.wrn": "f3dfdfa5cd9661fe5e95ef3580eef4c7437c069726421370c2d7328fbe7d840d",
|
||||
"packages/ui/styles/SelectStyles.wrn": "074fe0d67de4ef5e9f9cfe879beb72fd5c83352888687d3a1a4b7722c87af3ca",
|
||||
"packages/ui/ui.css": "9ea591404e9cf675bbf1003e15e9fad00327094ff217dc20733f0fc87c0f4a62"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "wrnexus",
|
||||
"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.",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.11",
|
||||
"publisher": "wrnexus",
|
||||
"private": true,
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
|
||||
+130
-20
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||
// WRN editor compiler source hash: 1e7593d6b6bd1c9b8439fc34a30f9db9f7ae64f1788365a74115170aa34cd185
|
||||
// WRN editor compiler generator hash: a54ca847c758bc98d8e353ad6d70088df31de1820f6cf9d1c3462505f563e6b8
|
||||
// WRN editor compiler source hash: 3d346a264807ba7b107584dc9a7b505b1d14057b2ad1aa85c5ea890f9b09c272
|
||||
// WRN editor compiler generator hash: f8c1094b47e695151205899de28cd63d574356d26ac49219c4a43db0f2391ef5
|
||||
// Generated with TypeScript: 6.0.3
|
||||
const __nodeRequire = require;
|
||||
const __path = __nodeRequire("node:path");
|
||||
@@ -1148,6 +1148,31 @@ function componentEventAttribute(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) {
|
||||
let found = false;
|
||||
const value = raw.replace(/\{([^{}]+)\}/g, (whole, inner) => {
|
||||
@@ -1166,6 +1191,7 @@ function reactiveAttrValue(raw, reactive) {
|
||||
return found ? value : null;
|
||||
}
|
||||
function renderAttrs(attrs, csrId, reactive = null, dynamicExpressions) {
|
||||
attrs = expandBindings(attrs);
|
||||
let bindIndex = 0;
|
||||
const rendered = attrs
|
||||
.map((attr) => {
|
||||
@@ -1302,7 +1328,7 @@ function renderLoopBody(node, locals = []) {
|
||||
return compileIfExpr(node, locals);
|
||||
}
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const name = attr.event
|
||||
@@ -1562,9 +1588,9 @@ function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindin
|
||||
const island = islandMarkerFor(node);
|
||||
if (island)
|
||||
return island;
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
||||
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
|
||||
.join("");
|
||||
const inner = node.children
|
||||
.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive))
|
||||
@@ -1577,7 +1603,7 @@ function renderNestedComponentInvocation(node, ctx) {
|
||||
if (island)
|
||||
return island;
|
||||
let bindIndex = 0;
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(attr.name);
|
||||
@@ -2446,6 +2472,8 @@ ${exposed
|
||||
${declarations}
|
||||
${visible.length
|
||||
? ` 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(", ")} };`
|
||||
: ""}
|
||||
}`;
|
||||
@@ -2765,7 +2793,7 @@ function renderClientControlTemplate(nodes) {
|
||||
}
|
||||
const componentTag = isComponentTag(node.tag);
|
||||
let bindIndex = 0;
|
||||
const attrs = node.attrs
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.map((attribute) => {
|
||||
const name = attribute.event
|
||||
? componentTag
|
||||
@@ -2803,19 +2831,21 @@ function encodeClientControl(value) {
|
||||
}
|
||||
function renderComponentIfNode(node, ctx) {
|
||||
let expression = "``";
|
||||
const clientBranches = [];
|
||||
for (let index = node.branches.length - 1; index >= 0; index--) {
|
||||
const branch = node.branches[index];
|
||||
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
|
||||
const bodyExpression = "`" + body + "`";
|
||||
clientBranches.unshift(`{ cond: ${branch.cond === null ? "null" : JSON.stringify(branch.cond)}, body: ${bodyExpression} }`);
|
||||
expression =
|
||||
branch.cond === null
|
||||
? bodyExpression
|
||||
: `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
|
||||
}
|
||||
const definition = encodeClientControl(node.branches.map((branch) => ({
|
||||
cond: branch.cond,
|
||||
body: renderClientControlTemplate(branch.body),
|
||||
})));
|
||||
// Render all branches on the server into an inert, encoded definition.
|
||||
// A branch first selected in the browser therefore contains the real nested
|
||||
// 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>`;
|
||||
}
|
||||
function renderComponentEachNode(node, ctx) {
|
||||
@@ -2955,7 +2985,7 @@ function renderComponentNode(node, ctx) {
|
||||
}
|
||||
}
|
||||
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:"))
|
||||
.map((a) => {
|
||||
const spread = /^\{\.\.\.([A-Za-z_$][\w$]*)\}$/.exec(a.name);
|
||||
@@ -3413,6 +3443,9 @@ function __wrnRaw(v: unknown): string {
|
||||
}`);
|
||||
}
|
||||
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 {
|
||||
if (value === undefined) {
|
||||
return "undefined";
|
||||
@@ -3495,7 +3528,7 @@ function __wrnProp(v) {
|
||||
const value = v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
||||
return __wrnAttr(value);
|
||||
}
|
||||
function renderPageComponentAttr(attr, dynamicExpressions) {
|
||||
function renderPageComponentAttr(attr, dynamicExpressions, _reactive) {
|
||||
if (attr.event) {
|
||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
@@ -3508,7 +3541,11 @@ function renderPageComponentAttr(attr, dynamicExpressions) {
|
||||
}
|
||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||
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}`;
|
||||
}
|
||||
|
||||
},
|
||||
@@ -4030,6 +4067,7 @@ function islandNamesFrom(imports) {
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.NativeCompileError = void 0;
|
||||
exports.generateNative = generateNative;
|
||||
const syntax_1 = require("@wrnexus/syntax");
|
||||
class NativeCompileError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
@@ -4095,12 +4133,12 @@ function expression(value) {
|
||||
function textJsx(value) {
|
||||
const pieces = [];
|
||||
let last = 0;
|
||||
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
||||
if (match.index > last)
|
||||
pieces.push(value.slice(last, match.index));
|
||||
const expr = match[1].trim();
|
||||
for (const segment of (0, syntax_1.extractInterpolations)(value)) {
|
||||
if (segment.start > last)
|
||||
pieces.push(value.slice(last, segment.start));
|
||||
const expr = segment.expression;
|
||||
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));
|
||||
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
||||
@@ -6190,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 = {}) {
|
||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||
const printWidth = options.printWidth ?? 100;
|
||||
@@ -6245,6 +6303,12 @@ function formatWrnPass(source, options = {}) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
|
||||
if (structuredProp) {
|
||||
output.push(...structuredProp);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const leadingClosingBraces = countLeadingClosingBraces(value);
|
||||
const closesControlBlock = isControlBlockClose(value);
|
||||
const continuesControlBlock = isControlBlockMiddle(value);
|
||||
@@ -6325,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);
|
||||
};
|
||||
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");
|
||||
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; } });
|
||||
@@ -6337,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, "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; } });
|
||||
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");
|
||||
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; } });
|
||||
@@ -6370,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, "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) {
|
||||
"use strict";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
// WRN editor language server source hash: 3c6060ff9db332cf1dc01a467d795fb2d002300400072a4b87437c48d61ab7f9
|
||||
// WRN editor language server source hash: 5258b3e76cec47df752d4bb886fbd57a3f3c4af9ca9b6b2bf6742c8d78025ff1
|
||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||
// @bun @bun-cjs
|
||||
(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)) : {};
|
||||
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
if (mod && typeof mod === "object" || typeof mod === "function") {
|
||||
for (let key of __getOwnPropNames(mod))
|
||||
if (!__hasOwnProp.call(to, key))
|
||||
__defProp(to, key, {
|
||||
get: __accessProp.bind(mod, key),
|
||||
enumerable: true
|
||||
});
|
||||
}
|
||||
if (canCache)
|
||||
cache.set(mod, to);
|
||||
return to;
|
||||
@@ -66,7 +68,7 @@ var __export = (target, all) => {
|
||||
};
|
||||
|
||||
// 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";
|
||||
/*! *****************************************************************************
|
||||
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
|
||||
var require_main = __commonJS((exports2, module2) => {
|
||||
var require_main = __commonJS(function(exports2, module2) {
|
||||
var __defProp2 = Object.defineProperty;
|
||||
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
||||
@@ -169658,9 +169660,9 @@ var require_main = __commonJS((exports2, module2) => {
|
||||
// packages/language-server/src/server.ts
|
||||
var exports_server = {};
|
||||
__export(exports_server, {
|
||||
isApiReferenceAt: () => isApiReferenceAt,
|
||||
apiCallCompletions: () => apiCallCompletions,
|
||||
apiCallHover: () => apiCallHover,
|
||||
apiCallCompletions: () => apiCallCompletions
|
||||
isApiReferenceAt: () => isApiReferenceAt
|
||||
});
|
||||
module.exports = __toCommonJS(exports_server);
|
||||
|
||||
@@ -170720,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 = {}) {
|
||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||
const printWidth = options.printWidth ?? 100;
|
||||
@@ -170776,6 +170798,12 @@ function formatWrnPass(source, options = {}) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const structuredProp = formatStructuredPropDeclaration(value, unit, codeDepth + htmlDepth + controlDepth, printWidth);
|
||||
if (structuredProp) {
|
||||
output.push(...structuredProp);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const leadingClosingBraces = countLeadingClosingBraces(value);
|
||||
const closesControlBlock = isControlBlockClose(value);
|
||||
const continuesControlBlock = isControlBlockMiddle(value);
|
||||
@@ -172391,6 +172419,44 @@ function parseHtmlView(src, pos) {
|
||||
const nodes = parseNodeList("root");
|
||||
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
|
||||
function stripAsciiControlAndSpace(value) {
|
||||
let result = "";
|
||||
@@ -172988,7 +173054,7 @@ function runtimeNamespace(runtime) {
|
||||
return `__wrn_${runtime}`;
|
||||
}
|
||||
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}}`;
|
||||
}
|
||||
function retainedImports(ast) {
|
||||
@@ -173048,6 +173114,27 @@ var RESERVED_BINDING_NAMES = new Set([
|
||||
function safeBindingName(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)) {
|
||||
const ast = parse(source);
|
||||
const chunks = [];
|
||||
@@ -173103,6 +173190,8 @@ function virtualTypeScriptModule(source, filePath = "component.wrn", appRoot = f
|
||||
append(`declare const api: { ${apiType} };`);
|
||||
append(`declare const props: Readonly<${ast.name}Props>;`);
|
||||
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(importedWrnDeclarations(ast, filePath, appRoot));
|
||||
const sharedNames = new Set(ast.runtimeFunctions.filter((fn) => fn.runtime === "shared").map((fn) => fn.name));
|
||||
@@ -173119,6 +173208,53 @@ ${sharedAliases}`);
|
||||
append(functionDeclaration(fn), fn.source);
|
||||
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 {
|
||||
ast,
|
||||
fileName: filePath.replace(/\.wrn$/i, ".wrn.ts"),
|
||||
@@ -173224,7 +173360,7 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
|
||||
const shape = shapes.get(node.tag);
|
||||
if (!shape)
|
||||
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 position = lineAt(source, `<${node.tag}`);
|
||||
for (const prop of shape.props) {
|
||||
@@ -173261,7 +173397,8 @@ function componentUsageDiagnostics(source, ast, filePath, appRoot) {
|
||||
}
|
||||
if (/^(?:class|id|style|slot|data-|aria-)/.test(attr.name) || attr.name === "attrs")
|
||||
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) {
|
||||
diagnostics.push({
|
||||
code: "WRN-COMPONENT-UNKNOWN-PROP",
|
||||
|
||||
@@ -20,6 +20,20 @@ test("preserves nested prop defaults while formatting", () => {
|
||||
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", () => {
|
||||
const source = `component Footer {
|
||||
state footerItems = [{"label":"Accessibility","href":"/accessibility","value":"accessibility"},{"label":"Privacy","href":"/privacy","value":"privacy"}]
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@
|
||||
"generate:ui-visual": "node scripts/check-ui-visual-contract.mjs --write",
|
||||
"sbom": "node scripts/generate-sbom.mjs",
|
||||
"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",
|
||||
"stage:packages": "bun run scripts/publish-packages.ts",
|
||||
"test:staged-consumers": "node scripts/test-staged-consumers.mjs",
|
||||
@@ -70,7 +70,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/bun": "^1.3.14",
|
||||
"@types/bun": "^1.4.0",
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.4",
|
||||
"eslint": "^10.8.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/authz",
|
||||
"version": "0.8.14",
|
||||
"version": "0.8.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -16,6 +16,11 @@ export function defineAuthz<Subject = any, Resource = any>(
|
||||
const bindings = module.bindings ?? {};
|
||||
|
||||
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("*")) {
|
||||
throw new Error(
|
||||
`WRN-AUTHZ-DECL: permission id '${id}' must not contain a wildcard; wildcards belong in roles.`,
|
||||
|
||||
@@ -26,6 +26,12 @@ describe("defineAuthz", () => {
|
||||
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", () => {
|
||||
expect(() => defineAuthz({ roles: { editor: [""] } })).toThrow(/role 'editor'/i);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/captcha",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.14",
|
||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||
"type": "module",
|
||||
"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",
|
||||
"version": "0.8.57",
|
||||
"version": "0.8.64",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./workspace": "./src/workspace.ts"
|
||||
"./workspace": "./src/workspace.ts",
|
||||
"./test-setup": "./src/test-setup.ts"
|
||||
},
|
||||
"bin": {
|
||||
"wrnexus": "src/index.ts"
|
||||
|
||||
@@ -113,6 +113,11 @@ Update options: --dry-run previews changes; --no-verify skips post-update check/
|
||||
async function main(): Promise<void> {
|
||||
const [command, ...rest] = process.argv.slice(2);
|
||||
|
||||
if (command === "--version" || command === "-v" || command === "version") {
|
||||
console.log(currentCliVersion());
|
||||
return;
|
||||
}
|
||||
|
||||
notifyIfUpdateAvailable(currentCliVersion(), command);
|
||||
|
||||
switch (command) {
|
||||
|
||||
@@ -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;
|
||||
files: string[];
|
||||
setup?: { command: string; args: string[] };
|
||||
env?: Record<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 level = args.find((value): value is TestLevel => TEST_LEVELS.includes(value as TestLevel));
|
||||
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 browsers = (args.find((value) => value.startsWith("--browsers="))?.slice(11) ?? "chromium")
|
||||
.split(",")
|
||||
@@ -120,10 +126,18 @@ export function createTestPlan(appRoot: string, args: string[]): TestCommandPlan
|
||||
);
|
||||
return {
|
||||
command: process.execPath,
|
||||
args: ["test", ...(watch ? ["--watch"] : []), ...(pattern ? files : []), ...passthrough],
|
||||
args: [
|
||||
"test",
|
||||
"--preload",
|
||||
setupModule,
|
||||
...(watch ? ["--watch"] : []),
|
||||
...(pattern ? files : []),
|
||||
...passthrough,
|
||||
],
|
||||
cwd: root,
|
||||
level,
|
||||
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;
|
||||
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) {
|
||||
const setup = spawn(plan.setup.command, plan.setup.args, { stdio: "inherit", cwd: plan.cwd });
|
||||
setup.on("exit", (code, signal) => {
|
||||
|
||||
@@ -63,14 +63,16 @@ const execFileAsync = promisify(execFile);
|
||||
|
||||
/** Resolve every installed WRNexus dependency independently and in parallel. */
|
||||
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 entries = await Promise.all(
|
||||
unique.map(async (name) => {
|
||||
try {
|
||||
const { stdout } = await execFileAsync(npm, ["view", name, "version", "--json"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const command = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "npm";
|
||||
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 version = Array.isArray(parsed) ? parsed.at(-1) : parsed;
|
||||
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>()
|
||||
: 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)" : ""}`);
|
||||
if (!versionArg) {
|
||||
console.log(
|
||||
|
||||
@@ -50,6 +50,15 @@ describe("test command planning", () => {
|
||||
test("keeps the unfiltered legacy command", async () => {
|
||||
const plan = createTestPlan(await fixture(), ["--watch"]);
|
||||
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 { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { compareVersions, notifyIfUpdateAvailable } from "../src/update-notifier.ts";
|
||||
import { join, resolve } from "node:path";
|
||||
import {
|
||||
compareVersions,
|
||||
currentCliVersion,
|
||||
notifyIfUpdateAvailable,
|
||||
} from "../src/update-notifier.ts";
|
||||
|
||||
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 });
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
"version": "0.8.19",
|
||||
"version": "0.8.22",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -834,7 +834,7 @@ function renderPageComponentInvocation(
|
||||
|
||||
const attrs = expandBindings(node.attrs)
|
||||
.filter((attr) => attr.name !== "data-component")
|
||||
.map((attr) => renderPageComponentAttr(attr, loops))
|
||||
.map((attr) => renderPageComponentAttr(attr, loops, reactive))
|
||||
.join("");
|
||||
|
||||
const inner = node.children
|
||||
@@ -1857,6 +1857,8 @@ ${declarations}
|
||||
${
|
||||
visible.length
|
||||
? ` 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(", ")} };`
|
||||
: ""
|
||||
}
|
||||
@@ -3170,7 +3172,11 @@ function __wrnProp(v: unknown): string {
|
||||
|
||||
return __wrnAttr(value);
|
||||
}
|
||||
function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): string {
|
||||
function renderPageComponentAttr(
|
||||
attr: Attr,
|
||||
dynamicExpressions: string[],
|
||||
_reactive: PageReactive | null,
|
||||
): string {
|
||||
if (attr.event) {
|
||||
return ` ${componentEventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
|
||||
}
|
||||
@@ -3188,6 +3194,12 @@ function renderPageComponentAttr(attr: Attr, dynamicExpressions: string[]): stri
|
||||
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
|
||||
|
||||
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 {
|
||||
constructor(message: string) {
|
||||
@@ -68,11 +68,11 @@ function expression(value: string): string | null {
|
||||
function textJsx(value: string): string {
|
||||
const pieces: string[] = [];
|
||||
let last = 0;
|
||||
for (const match of value.matchAll(/\{([^{}]+)\}/g)) {
|
||||
if (match.index! > last) pieces.push(value.slice(last, match.index));
|
||||
const expr = match[1]!.trim();
|
||||
for (const segment of extractInterpolations(value)) {
|
||||
if (segment.start > last) pieces.push(value.slice(last, segment.start));
|
||||
const expr = segment.expression;
|
||||
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));
|
||||
return pieces.join("").replace(/([<>])/g, (char) => (char === "<" ? "<" : ">"));
|
||||
|
||||
@@ -56,6 +56,17 @@ test("named loads support memoized dependencies and deferred execution", () => {
|
||||
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", () => {
|
||||
expect(() =>
|
||||
parse(
|
||||
|
||||
@@ -1151,6 +1151,16 @@ page Home {
|
||||
|
||||
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", () => {
|
||||
const output = generate(
|
||||
parse(`component Parent {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/csr",
|
||||
"version": "0.8.32",
|
||||
"version": "0.8.35",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -90,7 +90,7 @@ const CONTROLLER_LOADER = String.raw`
|
||||
if (!componentControllerPromise) {
|
||||
componentControllerPromise = new Promise(function (resolve, reject) {
|
||||
var script = document.createElement("script");
|
||||
script.src = componentControllerUrl;
|
||||
script.src = trustedScriptURL(componentControllerUrl);
|
||||
script.defer = true;
|
||||
script.onload = resolve;
|
||||
script.onerror = reject;
|
||||
|
||||
@@ -26,6 +26,40 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var behaviorObserver;
|
||||
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
|
||||
* 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) {
|
||||
var cssText = rule.cssText || "";
|
||||
var match;
|
||||
var pattern = /var\(\s*(--wrn-[A-Za-z0-9_-]+)/g;
|
||||
while ((match = pattern.exec(cssText))) referenced[match[1]] = true;
|
||||
// Only inspect declarations that can affect the current document. A
|
||||
// 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) {
|
||||
Array.prototype.forEach.call(rule.style, function (property) {
|
||||
if (String(property).indexOf("--wrn-") === 0) declared[property] = true;
|
||||
@@ -1818,7 +1865,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var next = markup == null ? "" : String(markup);
|
||||
|
||||
if (htmlNode.innerHTML !== next) {
|
||||
htmlNode.innerHTML = next;
|
||||
htmlNode.innerHTML = trustedHTML(next);
|
||||
}
|
||||
});
|
||||
runHtml();
|
||||
@@ -2002,7 +2049,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
function clearControlContent() {
|
||||
if (!rangeEnd) { block.innerHTML = ""; return; }
|
||||
if (!rangeEnd) { block.innerHTML = trustedHTML(""); return; }
|
||||
while (block.nextSibling && block.nextSibling !== rangeEnd) {
|
||||
block.parentNode.removeChild(block.nextSibling);
|
||||
}
|
||||
@@ -2010,7 +2057,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
|
||||
function appendControlContent(markup, locals) {
|
||||
var template = document.createElement("template");
|
||||
template.innerHTML = markup || "";
|
||||
template.innerHTML = trustedHTML(markup || "");
|
||||
var fragment = template.content;
|
||||
var elements = toArray(fragment.childNodes).filter(function (node) {
|
||||
return node.nodeType === 1;
|
||||
@@ -2399,7 +2446,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var markup = value == null ? "" : String(value);
|
||||
|
||||
if (node.innerHTML !== markup) {
|
||||
node.innerHTML = markup;
|
||||
node.innerHTML = trustedHTML(markup);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2775,9 +2822,15 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// server forwards these markers onto the rendered child root; evaluate
|
||||
// them here and write changes into the child's prop signals.
|
||||
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) {
|
||||
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) {
|
||||
if (attr.name.indexOf("data-wrn-prop-bind-") !== 0) return;
|
||||
var binding = pairBinding(attr.value);
|
||||
@@ -2796,9 +2849,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
});
|
||||
} catch (_) { return; }
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,13 @@ test("split runtime hydrates a controller only from the controller asset", () =>
|
||||
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", () => {
|
||||
const win = mount(
|
||||
`<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");
|
||||
});
|
||||
|
||||
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)", () => {
|
||||
const win = mount(
|
||||
`<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);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
const production = getReactiveRuntime();
|
||||
expect(production).not.toContain("WRN-DEV-");
|
||||
|
||||
@@ -15,9 +15,17 @@ function fakeDb() {
|
||||
|
||||
test("seed helpers parameterize object rows and removals", async () => {
|
||||
const { db, calls } = fakeDb();
|
||||
await addSeedData(db, [{ code: "free", credits: 10 }, { code: "pro", credits: 20 }], "plans", {
|
||||
conflict: "ignore",
|
||||
});
|
||||
await addSeedData(
|
||||
db,
|
||||
[
|
||||
{ code: "free", credits: 10 },
|
||||
{ code: "pro", credits: 20 },
|
||||
],
|
||||
"plans",
|
||||
{
|
||||
conflict: "ignore",
|
||||
},
|
||||
);
|
||||
await removeSeedData(db, { code: "free" }, "plans");
|
||||
expect(calls[0]).toEqual({
|
||||
sql: "INSERT OR IGNORE INTO plans (code, credits) VALUES (?, ?)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.50",
|
||||
"version": "0.8.56",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
@@ -25,6 +25,7 @@
|
||||
"@wrnexus/uploader": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
"@wrnexus/queue": "workspace:*",
|
||||
"@wrnexus/react": "workspace:*",
|
||||
"@wrnexus/store": "workspace:*",
|
||||
"@wrnexus/security": "workspace:*",
|
||||
"@wrnexus/observability": "workspace:*",
|
||||
|
||||
@@ -367,6 +367,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
externalRoutes: pluginContributions.routes,
|
||||
middlewareFiles: pluginContributions.middleware,
|
||||
});
|
||||
// UI and plugin components are intentionally registered application-wide;
|
||||
// using them without a source import is not an implicit-import diagnostic.
|
||||
setCompileImportOptions(appRoot, {
|
||||
...importConfig,
|
||||
globalComponents: router.components.map((component) => component.name),
|
||||
});
|
||||
const styleEntry = opts.styleEntry ?? null;
|
||||
|
||||
const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar);
|
||||
|
||||
@@ -86,6 +86,7 @@ interface CompileImportOptions {
|
||||
mode: ImportMode;
|
||||
aliases: Record<string, string>;
|
||||
autoImport: boolean;
|
||||
globalComponents: Set<string>;
|
||||
}
|
||||
const compileImportOptions = new Map<string, CompileImportOptions>();
|
||||
const warnedImportDiagnostics = new Set<string>();
|
||||
@@ -104,12 +105,18 @@ export function setDevCompilerPipeline(pipeline: DevCompilerPipeline | null): vo
|
||||
|
||||
export function setCompileImportOptions(
|
||||
appRoot: string,
|
||||
options: { mode?: ImportMode; aliases?: Record<string, string>; autoImport?: boolean } = {},
|
||||
options: {
|
||||
mode?: ImportMode;
|
||||
aliases?: Record<string, string>;
|
||||
autoImport?: boolean;
|
||||
globalComponents?: Iterable<string>;
|
||||
} = {},
|
||||
): void {
|
||||
compileImportOptions.set(resolve(appRoot), {
|
||||
mode: options.mode ?? "compatible",
|
||||
aliases: { "@": "./app", ...(options.aliases ?? {}) },
|
||||
autoImport: options.autoImport ?? true,
|
||||
globalComponents: new Set(options.globalComponents ?? []),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,11 +148,18 @@ function importOptionsHash(file: string): string {
|
||||
mode: "compatible" as const,
|
||||
aliases: { "@": "./app" },
|
||||
autoImport: true,
|
||||
globalComponents: new Set<string>(),
|
||||
};
|
||||
const aliases = Object.fromEntries(
|
||||
Object.entries(options.aliases).sort(([left], [right]) => left.localeCompare(right)),
|
||||
);
|
||||
return hashPath(JSON.stringify({ ...options, aliases }));
|
||||
return hashPath(
|
||||
JSON.stringify({
|
||||
...options,
|
||||
aliases,
|
||||
globalComponents: [...options.globalComponents].sort(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Island bundles already built this dev session, keyed by resolved source. */
|
||||
@@ -493,7 +507,8 @@ function validateConfiguredImports(_source: string, ast: PageAst, file: string):
|
||||
"Transition",
|
||||
]);
|
||||
const missing = [...usedComponents].filter(
|
||||
(name) => !compilerBuiltins.has(name) && !imported.has(name),
|
||||
(name) =>
|
||||
!compilerBuiltins.has(name) && !options.globalComponents.has(name) && !imported.has(name),
|
||||
);
|
||||
if (ast.layoutIsSymbol && ast.layout && !imported.has(ast.layout)) missing.push(ast.layout);
|
||||
if (!missing.length) return;
|
||||
|
||||
@@ -1739,6 +1739,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
// Props may carry `{t:key}` i18n markers — resolve them for the active
|
||||
// language before handing them to the component.
|
||||
const props = resolveTProps(parseComponentProps(attrStr!), translate);
|
||||
// Client control templates retain reactive expressions (for example
|
||||
// `metrics="{metrics}"`) because their parent state is not available
|
||||
// while we pre-render the inert branch metadata. Do not feed those
|
||||
// expression strings into typed component coercion. Rendering with the
|
||||
// component defaults creates the real child boundary; the bridged prop
|
||||
// binding synchronizes the live value when the branch is instantiated.
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (/^\{[\s\S]*\}$/.test(value) && !/^\{t:/.test(value)) delete props[key];
|
||||
}
|
||||
if (normalizedName === "languageswitcher" && deps.i18n) {
|
||||
props.locales ??= JSON.stringify(
|
||||
deps.i18n.langs.map((locale) => ({
|
||||
@@ -1769,7 +1778,20 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
},
|
||||
)
|
||||
: renderComponent();
|
||||
result += await renderComponents(rendered, translate, language, depth + 1);
|
||||
// Prop bindings are authored by the parent mount but consumed by the
|
||||
// rendered child's scope. The mount itself is replaced during SSR, so
|
||||
// carry those reserved attributes onto the child's scope root.
|
||||
const propBindings = Array.from(
|
||||
attrStr!.matchAll(/\s(data-wrn-prop-bind-[A-Za-z0-9_-]+(?:="[^"]*")?)/g),
|
||||
(match) => ` ${match[1]}`,
|
||||
).join("");
|
||||
const bridged = propBindings
|
||||
? rendered.replace(
|
||||
/(<[A-Za-z][A-Za-z0-9-]*\b[^>]*\bdata-scope="[^"]*")/,
|
||||
`$1${propBindings}`,
|
||||
)
|
||||
: rendered;
|
||||
result += await renderComponents(bridged, translate, language, depth + 1);
|
||||
} catch (err) {
|
||||
console.error(`[wrnexus] component '${name}' failed to render`, err);
|
||||
deps.devToolbar?.collector.add(
|
||||
@@ -1950,6 +1972,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
||||
).getOrLoad(`route:${matched.route.raw}`, () => load(pageCtx));
|
||||
}
|
||||
(pageCtx as Context & { data?: unknown }).data = data;
|
||||
if (data instanceof Response) return data;
|
||||
if (data && typeof data === "object") Object.assign(pageCtx, data);
|
||||
}
|
||||
let body = await renderComponents(String(await component(pageCtx)), ctx.t, ctx.lang);
|
||||
@@ -2668,7 +2691,12 @@ export function parseComponentProps(attrStr: string): Record<string, string> {
|
||||
const props: Record<string, string> = {};
|
||||
for (const m of attrStr.matchAll(/([A-Za-z_][\w-]*)(?:="([^"]*)")?/g)) {
|
||||
const key = m[1]!;
|
||||
if (key === "data-component") continue;
|
||||
if (
|
||||
key === "data-component" ||
|
||||
key.startsWith("data-wrn-bind-") ||
|
||||
key.startsWith("data-wrn-prop-bind-")
|
||||
)
|
||||
continue;
|
||||
props[key] = decodeHtmlEntities(m[2] ?? "");
|
||||
}
|
||||
return props;
|
||||
|
||||
@@ -90,6 +90,92 @@ test("a component in an initially hidden client branch is server-prepared for in
|
||||
expect(branches[0].body).not.toContain("data-component=");
|
||||
});
|
||||
|
||||
test("typed components in client branches use defaults until reactive props are available", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-reactive-branch-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "components"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
|
||||
writeFileSync(join(app, "components/Panel.wrn"), "component Panel { view { <section /> } }\n");
|
||||
const marker = "["items","{items}"]";
|
||||
const definition = Buffer.from(
|
||||
JSON.stringify([
|
||||
{
|
||||
cond: "open",
|
||||
body: `<div data-component="Panel" items="{items}" visible="{true}" data-wrn-prop-bind-0="${marker}"></div>`,
|
||||
},
|
||||
]),
|
||||
"utf8",
|
||||
).toString("base64");
|
||||
let observed: Record<string, string> | undefined;
|
||||
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Panel.wrn"
|
||||
? {
|
||||
render: (props: Record<string, string>) => {
|
||||
observed = props;
|
||||
return '<section data-scope="items: [], visible: false" data-wrn-scope="e30="></section>';
|
||||
},
|
||||
}
|
||||
: {
|
||||
default: () =>
|
||||
`<template data-wrn-if="${definition}"></template><template data-wrn-control-end></template>`,
|
||||
},
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
|
||||
const response = await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
const html = await response!.text();
|
||||
expect(observed).toEqual({});
|
||||
const encoded = /data-wrn-if="([A-Za-z0-9+/=]+)"/.exec(html)?.[1];
|
||||
const branches = JSON.parse(Buffer.from(encoded!, "base64").toString("utf8"));
|
||||
expect(branches[0].body).toContain("<section");
|
||||
expect(branches[0].body).toContain("data-wrn-prop-bind-0");
|
||||
expect(branches[0].body).not.toContain("data-component=");
|
||||
});
|
||||
|
||||
test("SSR carries a parent prop binding onto the rendered child scope", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-component-prop-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
mkdirSync(join(app, "components"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
|
||||
writeFileSync(
|
||||
join(app, "components/Child.wrn"),
|
||||
"component Child { props { value = 0 } view { <b>{value}</b> } }\n",
|
||||
);
|
||||
const marker = "["value","{count}"]";
|
||||
const handlers = createHandlers({
|
||||
mode: "development",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async (file) =>
|
||||
basename(file) === "Child.wrn"
|
||||
? { render: () => '<div data-scope="value: 1" data-wrn-scope="e30="><b>1</b></div>' }
|
||||
: {
|
||||
default: () =>
|
||||
`<div data-component="Child" value="1" data-wrn-prop-bind-0="${marker}"></div>`,
|
||||
},
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const response = await handlers.fetch(new Request("https://example.test/"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
const html = await response!.text();
|
||||
expect(html).toContain(`data-wrn-prop-bind-0="${marker}"`);
|
||||
expect(html).not.toContain('data-component="Child"');
|
||||
});
|
||||
|
||||
test("translated text is present in the raw server response", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-ssr-i18n-"));
|
||||
roots.push(root);
|
||||
|
||||
@@ -71,6 +71,13 @@ test("compiler-native reactive elements do not require application imports", ()
|
||||
expect(() => compileWrnArtifacts(page, 5)).not.toThrow();
|
||||
});
|
||||
|
||||
test("globally registered plugin components do not require source imports", () => {
|
||||
const { root, page } = fixture();
|
||||
writeFileSync(page, "page Home { view { <SignIn /> } }");
|
||||
setCompileImportOptions(root, { mode: "explicit", globalComponents: ["SignIn"] });
|
||||
expect(() => compileWrnArtifacts(page, 6)).not.toThrow();
|
||||
});
|
||||
|
||||
test("component examples in comments do not require imports", () => {
|
||||
const { root, page } = fixture();
|
||||
writeFileSync(
|
||||
|
||||
@@ -32,6 +32,32 @@ test("server loader data is available to page rendering", async () => {
|
||||
expect(await response!.text()).toContain("Ada,Lin / 2");
|
||||
});
|
||||
|
||||
test("a server loader can return a redirect Response", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-load-redirect-"));
|
||||
roots.push(root);
|
||||
const app = join(root, "app");
|
||||
mkdirSync(join(app, "pages"), { recursive: true });
|
||||
writeFileSync(join(app, "pages/private.ts"), "export default () => '';\n");
|
||||
const handlers = createHandlers({
|
||||
mode: "production",
|
||||
hmr: false,
|
||||
router: buildRouter(app),
|
||||
loadModule: async () => ({
|
||||
__wrnexusLoad: async () => Response.redirect("https://example.test/login", 303),
|
||||
default: () => {
|
||||
throw new Error("redirected pages must not render");
|
||||
},
|
||||
}),
|
||||
getMiddleware: async () => [],
|
||||
assets: { serve: async () => null },
|
||||
} satisfies RuntimeDeps);
|
||||
const response = await handlers.fetch(new Request("https://example.test/private"), {
|
||||
upgrade: () => false,
|
||||
});
|
||||
expect(response?.status).toBe(303);
|
||||
expect(response?.headers.get("location")).toBe("https://example.test/login");
|
||||
});
|
||||
|
||||
test("HMR page synchronization initializes request-scoped loader caching", async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-load-runtime-"));
|
||||
roots.push(root);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.16",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createServerIssue, issueFromError } from "../../src/server/issues.ts";
|
||||
|
||||
test("server issues have stable fingerprints and retain actionable error context", () => {
|
||||
const input = {
|
||||
ruleId: "server/query",
|
||||
category: "server" as const,
|
||||
severity: "warning" as const,
|
||||
title: "Slow query",
|
||||
message: "Query exceeded budget",
|
||||
pathname: "/orders",
|
||||
source: { file: "app/api/orders.ts", line: 12, column: 4 },
|
||||
};
|
||||
const first = createServerIssue(input);
|
||||
const second = createServerIssue(input);
|
||||
expect(first.id).not.toBe(second.id);
|
||||
expect(first.fingerprint).toBe(second.fingerprint);
|
||||
expect(first.metadata?.pathname).toBe("/orders");
|
||||
|
||||
const error = issueFromError(new Error("database offline"), { pathname: "/orders" });
|
||||
expect(error.severity).toBe("error");
|
||||
expect(error.message).toBe("database offline");
|
||||
expect(error.metadata?.stack).toContain("database offline");
|
||||
expect(error.recommendation).toContain("stack trace");
|
||||
});
|
||||
|
||||
test("non-Error throws are normalized without losing caller context", () => {
|
||||
const issue = issueFromError(
|
||||
{ reason: "offline" },
|
||||
{
|
||||
ruleId: "server/dependency",
|
||||
title: "Dependency failed",
|
||||
pathname: "/health",
|
||||
source: { file: "app/api/health.ts", line: 4 },
|
||||
},
|
||||
);
|
||||
expect(issue.ruleId).toBe("server/dependency");
|
||||
expect(issue.title).toBe("Dependency failed");
|
||||
expect(issue.message).toBe("[object Object]");
|
||||
expect(issue.fingerprint).toContain("app/api/health.ts:4:0");
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/mail",
|
||||
"version": "0.8.1",
|
||||
"version": "0.8.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -8,6 +8,7 @@
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/encryption": "workspace:*",
|
||||
"@wrnexus/queue": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
+114
-5
@@ -1,4 +1,5 @@
|
||||
import type { DefinedJob } from "@wrnexus/queue";
|
||||
import { createKeyring, open, seal, type EncryptionKeyring } from "@wrnexus/encryption";
|
||||
|
||||
export interface MailMessage {
|
||||
from?: string;
|
||||
@@ -14,6 +15,41 @@ export interface MailDriver {
|
||||
test?(): Promise<{ ok: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
export interface SealedMailDriverOptions {
|
||||
/** Ciphertext stored by the application; plaintext is never retained here. */
|
||||
sealedCredential: string;
|
||||
unseal(value: string): Promise<string>;
|
||||
create(credential: string): MailDriver | Promise<MailDriver>;
|
||||
/** Cache the initialized driver. Defaults to true. */
|
||||
cache?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily opens a sealed credential only at the transport boundary. This keeps
|
||||
* encryption policy and SMTP/vendor choice independent while preventing every
|
||||
* app from rebuilding the same decrypt-on-send lifecycle.
|
||||
*/
|
||||
export function sealedMailDriver(options: SealedMailDriverOptions): MailDriver {
|
||||
let cached: Promise<MailDriver> | undefined;
|
||||
const create = async () => options.create(await options.unseal(options.sealedCredential));
|
||||
const driver = () =>
|
||||
options.cache === false
|
||||
? create()
|
||||
: (cached ??= create().catch((error) => {
|
||||
cached = undefined;
|
||||
throw error;
|
||||
}));
|
||||
return {
|
||||
async send(message) {
|
||||
return (await driver()).send(message);
|
||||
},
|
||||
async test() {
|
||||
const resolved = await driver();
|
||||
return resolved.test?.() ?? { ok: true };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface MailTemplate<T = Record<string, unknown>> {
|
||||
subject(data: T): string;
|
||||
html?(data: T): string;
|
||||
@@ -26,14 +62,87 @@ export interface MailOptions {
|
||||
sandbox?: { enabled?: boolean; allowlist?: readonly string[] };
|
||||
}
|
||||
|
||||
export function defineMail(options: MailOptions) {
|
||||
export interface MailSandboxDecision {
|
||||
allowed: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/** Fail-closed, case-insensitive sandbox policy shared by mail workers and defineMail. */
|
||||
export function mailSandboxDecision(
|
||||
sandbox: { enabled?: boolean | number; allowlist?: readonly string[] | string },
|
||||
recipient: unknown,
|
||||
): MailSandboxDecision {
|
||||
if (!sandbox.enabled) return { allowed: true };
|
||||
if (typeof recipient !== "string" || !recipient.trim()) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: "sandbox mode is on and no valid recipient address was given",
|
||||
};
|
||||
}
|
||||
let source: unknown = sandbox.allowlist ?? [];
|
||||
if (typeof source === "string") {
|
||||
try {
|
||||
source = JSON.parse(source || "[]");
|
||||
} catch {
|
||||
source = [];
|
||||
}
|
||||
}
|
||||
const allowed = new Set(
|
||||
(options.sandbox?.allowlist ?? []).map((value) => value.trim().toLowerCase()),
|
||||
(Array.isArray(source) ? source : [])
|
||||
.filter((value): value is string => typeof value === "string")
|
||||
.map((value) => value.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
const normalized = recipient.trim().toLowerCase();
|
||||
return allowed.has(normalized)
|
||||
? { allowed: true }
|
||||
: {
|
||||
allowed: false,
|
||||
reason: `sandbox mode is on and ${recipient} is not in the sandbox allow-list`,
|
||||
};
|
||||
}
|
||||
|
||||
export interface SealedMailCredentialsOptions {
|
||||
/** Environment variable holding a base64 AES key. */
|
||||
env?: string;
|
||||
keyId?: string;
|
||||
}
|
||||
|
||||
/** Owns validated, lazy encryption-key setup for credentials stored at rest. */
|
||||
export function defineSealedMailCredentials(options: SealedMailCredentialsOptions = {}) {
|
||||
const env = options.env ?? "APP_ENCRYPTION_KEY";
|
||||
let cached: EncryptionKeyring | undefined;
|
||||
const keyring = () => {
|
||||
if (cached) return cached;
|
||||
const secret = process.env[env];
|
||||
if (!secret)
|
||||
throw new Error(`${env} is not set; it is required to seal mail credentials at rest.`);
|
||||
let length: number;
|
||||
try {
|
||||
length = atob(secret).length;
|
||||
} catch {
|
||||
throw new Error(`${env} must be a valid base64-encoded 16, 24 or 32 byte key.`);
|
||||
}
|
||||
if (![16, 24, 32].includes(length)) {
|
||||
throw new Error(
|
||||
`${env} must decode to 16, 24 or 32 bytes; the current value decodes to ${length}.`,
|
||||
);
|
||||
}
|
||||
return (cached = createKeyring([{ id: options.keyId ?? "primary", secret, active: true }]));
|
||||
};
|
||||
return {
|
||||
keyring,
|
||||
seal: (plain: string) => seal(plain, keyring()),
|
||||
open: (sealed: string) => open(sealed, keyring()),
|
||||
};
|
||||
}
|
||||
|
||||
export function defineMail(options: MailOptions) {
|
||||
const assertAllowed = (recipients: readonly string[]) => {
|
||||
if (!options.sandbox?.enabled) return;
|
||||
const denied = recipients.find((recipient) => !allowed.has(recipient.trim().toLowerCase()));
|
||||
if (denied) throw new Error(`WRN-MAIL-SANDBOX: recipient '${denied}' is not allow-listed`);
|
||||
for (const recipient of recipients) {
|
||||
const decision = mailSandboxDecision(options.sandbox ?? {}, recipient);
|
||||
if (!decision.allowed) throw new Error(`WRN-MAIL-SANDBOX: ${decision.reason}`);
|
||||
}
|
||||
};
|
||||
return {
|
||||
async send(message: MailMessage) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { defineMail, defineMailTemplate } from "../src/index.ts";
|
||||
import {
|
||||
defineMail,
|
||||
defineMailTemplate,
|
||||
defineSealedMailCredentials,
|
||||
mailSandboxDecision,
|
||||
sealedMailDriver,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("mail templates render and sandbox blocks accidental recipients", async () => {
|
||||
const sent: unknown[] = [];
|
||||
@@ -16,3 +22,52 @@ test("mail templates render and sandbox blocks accidental recipients", async ()
|
||||
expect(sent).toHaveLength(1);
|
||||
await expect(mail.send({ to: "real@example.com", subject: "no" })).rejects.toThrow("SANDBOX");
|
||||
});
|
||||
|
||||
test("mail owns fail-closed sandbox decisions and sealed credential setup", async () => {
|
||||
expect(
|
||||
mailSandboxDecision({ enabled: 1, allowlist: '["Test@Example.com"]' }, "test@example.com"),
|
||||
).toEqual({ allowed: true });
|
||||
expect(
|
||||
mailSandboxDecision({ enabled: true, allowlist: "not-json" }, "x@example.com").allowed,
|
||||
).toBe(false);
|
||||
const previous = process.env.TEST_MAIL_ENCRYPTION_KEY;
|
||||
process.env.TEST_MAIL_ENCRYPTION_KEY = btoa(String.fromCharCode(...new Uint8Array(32).fill(7)));
|
||||
try {
|
||||
const credentials = defineSealedMailCredentials({ env: "TEST_MAIL_ENCRYPTION_KEY" });
|
||||
const sealed = await credentials.seal("smtp-secret");
|
||||
expect(sealed).not.toContain("smtp-secret");
|
||||
expect(await credentials.open(sealed)).toBe("smtp-secret");
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.TEST_MAIL_ENCRYPTION_KEY;
|
||||
else process.env.TEST_MAIL_ENCRYPTION_KEY = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("sealed drivers decrypt lazily once and delegate send/test", async () => {
|
||||
let opens = 0;
|
||||
const sent: string[] = [];
|
||||
const driver = sealedMailDriver({
|
||||
sealedCredential: "ciphertext",
|
||||
async unseal(value) {
|
||||
opens++;
|
||||
expect(value).toBe("ciphertext");
|
||||
return "plaintext-secret";
|
||||
},
|
||||
create(credential) {
|
||||
expect(credential).toBe("plaintext-secret");
|
||||
return {
|
||||
async send(message) {
|
||||
sent.push(message.subject);
|
||||
},
|
||||
async test() {
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(opens).toBe(0);
|
||||
await driver.send({ to: "safe@example.test", subject: "one" });
|
||||
expect(await driver.test?.()).toEqual({ ok: true });
|
||||
expect(opens).toBe(1);
|
||||
expect(sent).toEqual(["one"]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# @wrnexus/metering
|
||||
|
||||
Framework-native entitlement catalogs, credit packs, and usage metering for WrNexus applications.
|
||||
|
||||
The package keeps policy separate from persistence: applications provide a `MeterStore`, while the metering facade validates unit amounts and reports insufficient balances separately from storage faults. All amounts are positive safe integers except explicit adjustments, which may be positive or negative but never zero.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { defineEntitlements, defineMeter, definePacks } from "@wrnexus/metering";
|
||||
|
||||
const entitlements = defineEntitlements({
|
||||
plans: () => [
|
||||
{ code: "free", name: "Free", features: [], allowance: 10 },
|
||||
{ code: "pro", name: "Pro", features: ["exports"], allowance: 1_000 },
|
||||
],
|
||||
subscriptionFor: async (subjectId) => subscriptions.planFor(subjectId),
|
||||
fallback: "free",
|
||||
});
|
||||
|
||||
const packs = definePacks([
|
||||
{ code: "starter", units: 100 },
|
||||
{ code: "plus", units: 500 },
|
||||
]);
|
||||
|
||||
const meter = defineMeter({ store });
|
||||
|
||||
if (await entitlements.enabled("user-1", "exports")) {
|
||||
const result = await meter.reserve("user-1", 1, "export report");
|
||||
if (!result.ok) console.error(result.reason);
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
- `defineEntitlements(options)` resolves a subject's plan, features, and allowance from a server-owned catalog, with an explicit fallback plan.
|
||||
- `definePacks(entries)` creates an immutable, prototype-safe lookup of purchasable unit packs. Clients select a pack code rather than supplying an amount.
|
||||
- `defineMeter(options)` exposes `balance`, `reserve`, `grant`, `purchase`, `refund`, and `adjust` operations over an application-provided store.
|
||||
|
||||
Meter operations return `{ ok: true }` on success or `{ ok: false, reason, fault? }` on refusal. A result with `fault: true` identifies an underlying storage error; use `onFault` for operational reporting without exposing exception-based control flow to callers.
|
||||
@@ -0,0 +1,7 @@
|
||||
# @wrnexus/payment
|
||||
|
||||
Gateway-neutral, webhook-authoritative payments for WrNexus. Amounts are integer minor units. Every initialize and refund operation requires an idempotency key. The package never accepts or stores card numbers, CVVs, or expiry input; its components navigate to provider-hosted checkout.
|
||||
|
||||
Built-in adapters: offline `sandbox`, Stripe, Razorpay, and PayPal. Applications may add adapters with `defineGateway`. Capabilities are explicit, unsupported operations are refused, signed webhooks are mandatory in production, and package-owned migrations provision the append-only event ledger.
|
||||
|
||||
Webhook events are deduplicated by the provider's event ID and reduced in provider occurrence/sequence order. Captured and refund states are monotonic, so a delayed retry of an older event cannot regress a terminal payment.
|
||||
@@ -0,0 +1,8 @@
|
||||
component PayNow {
|
||||
props { checkoutUrl: string label?: string = "Pay now" disabled?: boolean = false }
|
||||
view {
|
||||
<a class="wrn-payment-button" href={checkoutUrl} aria-disabled={disabled}>
|
||||
{label}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
component PaymentHistory {
|
||||
props { payments: array = [] }
|
||||
view { <ol class="wrn-payment-history">{#each payments as payment}<li>{payment.currency} {payment.amount} — {payment.status}</li>{/each}</ol> }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
component PaymentMethodList {
|
||||
props { methods: array = [] }
|
||||
view { <ul class="wrn-payment-methods">{#each methods as method}<li>{method.brand} ending {method.last4}</li>{/each}</ul> }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
component PaymentSheet {
|
||||
props { checkoutUrl: string gateway: string }
|
||||
view { <section class="wrn-payment-sheet" data-gateway={gateway}><p>Secure payment is completed on the payment provider.</p><a href={checkoutUrl}>Continue securely</a></section> }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
component PaymentStatus {
|
||||
props { status: string label?: string = "Payment status" }
|
||||
view { <div class="wrn-payment-status" role="status"><strong>{label}</strong><span>{status}</span></div> }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
component PaymentSummary {
|
||||
props { captured: number = 0 refunded: number = 0 net: number = 0 currency: string }
|
||||
view { <dl><dt>Captured</dt><dd>{currency} {captured}</dd><dt>Refunded</dt><dd>{currency} {refunded}</dd><dt>Net</dt><dd>{currency} {net}</dd></dl> }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
component RefundButton {
|
||||
props { paymentId: string refundable: number partial?: boolean = false action?: string = "/api/payments/refund" }
|
||||
view { <form method="post" action={action}><input type="hidden" name="paymentId" value={paymentId} /><label>Reason<input name="reason" required /></label>{#if partial}<label>Amount<input name="amount" type="number" min="1" max={refundable} /></label>{/if}<button type="submit">Refund</button></form> }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@wrnexus/payment",
|
||||
"version": "0.8.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"files": [
|
||||
"src",
|
||||
"components",
|
||||
"README.md"
|
||||
],
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./adapters": "./src/adapters.ts",
|
||||
"./plugin": "./src/plugin.ts",
|
||||
"./runtime-webhook": "./src/runtime-webhook.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/db": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
import { defineGateway } from "./core.ts";
|
||||
import type {
|
||||
GatewayCapabilities,
|
||||
GatewayIntent,
|
||||
GatewayRefund,
|
||||
PaymentGateway,
|
||||
PaymentStatus,
|
||||
} from "./types.ts";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const hex = (bytes: ArrayBuffer) =>
|
||||
[...new Uint8Array(bytes)].map((value) => value.toString(16).padStart(2, "0")).join("");
|
||||
async function hmac(body: string, secret: string) {
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
return hex(await crypto.subtle.sign("HMAC", key, encoder.encode(body)));
|
||||
}
|
||||
function equal(left: string, right: string) {
|
||||
if (left.length !== right.length) return false;
|
||||
let difference = 0;
|
||||
for (let i = 0; i < left.length; i++) difference |= left.charCodeAt(i) ^ right.charCodeAt(i);
|
||||
return difference === 0;
|
||||
}
|
||||
const CAPABILITIES: GatewayCapabilities = {
|
||||
authorizeThenCapture: false,
|
||||
partialRefund: true,
|
||||
multipleRefunds: true,
|
||||
storedMethods: false,
|
||||
customerVault: false,
|
||||
hostedFields: false,
|
||||
hostedCheckout: true,
|
||||
webhookSignature: true,
|
||||
payouts: false,
|
||||
disputes: false,
|
||||
};
|
||||
const statusOf = (value: string): PaymentStatus =>
|
||||
(({
|
||||
succeeded: "captured",
|
||||
captured: "captured",
|
||||
paid: "captured",
|
||||
authorized: "authorized",
|
||||
requires_capture: "authorized",
|
||||
cancelled: "cancelled",
|
||||
canceled: "cancelled",
|
||||
failed: "failed",
|
||||
refunded: "refunded",
|
||||
})[value] as PaymentStatus | undefined) ?? "pending";
|
||||
|
||||
export interface SandboxOptions {
|
||||
secret?: string;
|
||||
failure?: "create" | "refund" | "capture";
|
||||
authorize?: boolean;
|
||||
}
|
||||
export function sandboxGateway(options: SandboxOptions = {}): PaymentGateway {
|
||||
const intents = new Map<string, GatewayIntent>();
|
||||
const refunds = new Map<string, GatewayRefund>();
|
||||
return defineGateway({
|
||||
id: "sandbox",
|
||||
capabilities: {
|
||||
...CAPABILITIES,
|
||||
authorizeThenCapture: true,
|
||||
storedMethods: true,
|
||||
customerVault: true,
|
||||
hostedFields: true,
|
||||
},
|
||||
supports: { currencies: "any", countries: "any" },
|
||||
async createIntent(input) {
|
||||
if (options.failure === "create") throw new Error("sandbox create failure");
|
||||
const value = {
|
||||
ref: `sb_${input.id}`,
|
||||
status: (options.authorize || input.capture === "manual"
|
||||
? "authorized"
|
||||
: "pending") as PaymentStatus,
|
||||
amount: input.amount,
|
||||
currency: input.currency,
|
||||
checkoutUrl: `https://sandbox.wrnexus.test/pay/${input.id}`,
|
||||
clientSecret: `sandbox_${input.id}`,
|
||||
};
|
||||
intents.set(value.ref, value);
|
||||
return value;
|
||||
},
|
||||
async fetchIntent(ref) {
|
||||
const value = intents.get(ref);
|
||||
if (!value) throw new Error("sandbox intent not found");
|
||||
return structuredClone(value);
|
||||
},
|
||||
async cancelIntent(ref) {
|
||||
const value = await this.fetchIntent(ref);
|
||||
const next = { ...value, status: "cancelled" as const };
|
||||
intents.set(ref, next);
|
||||
return next;
|
||||
},
|
||||
async capture(ref, amount) {
|
||||
if (options.failure === "capture") throw new Error("sandbox capture failure");
|
||||
const value = await this.fetchIntent(ref);
|
||||
const next = {
|
||||
...value,
|
||||
status: "captured" as const,
|
||||
amount: amount?.amount ?? value.amount,
|
||||
};
|
||||
intents.set(ref, next);
|
||||
return next;
|
||||
},
|
||||
async refund(input) {
|
||||
if (options.failure === "refund") throw new Error("sandbox refund failure");
|
||||
const existing = refunds.get(input.idempotencyKey);
|
||||
if (existing) return existing;
|
||||
const value = {
|
||||
ref: `sbr_${crypto.randomUUID()}`,
|
||||
status: "succeeded" as const,
|
||||
amount: input.amount,
|
||||
};
|
||||
refunds.set(input.idempotencyKey, value);
|
||||
return value;
|
||||
},
|
||||
async verifyWebhook(req, expected) {
|
||||
const body = await req.text();
|
||||
const signature = req.headers.get("x-wrnexus-signature") ?? "";
|
||||
if (!equal(signature, await hmac(body, expected)))
|
||||
throw new Error("invalid sandbox signature");
|
||||
const payload = JSON.parse(body);
|
||||
const intent = intents.get(payload.intentRef);
|
||||
if (intent) intents.set(payload.intentRef, { ...intent, status: payload.status });
|
||||
return {
|
||||
id: String(payload.id),
|
||||
intentRef: String(payload.intentRef),
|
||||
type: String(payload.type ?? "payment.updated"),
|
||||
status: payload.status,
|
||||
occurredAt: Number(payload.occurredAt ?? Date.now()),
|
||||
sequence: payload.sequence,
|
||||
payload,
|
||||
};
|
||||
},
|
||||
async listMethods() {
|
||||
return [];
|
||||
},
|
||||
async attachMethod(_customer, token) {
|
||||
return { ref: `sbm_${token}`, brand: "sandbox", last4: token.slice(-4) };
|
||||
},
|
||||
async detachMethod() {},
|
||||
async createCustomer(input) {
|
||||
return `sb_customer_${input.subject}`;
|
||||
},
|
||||
clientConfig(intent) {
|
||||
return {
|
||||
gateway: "sandbox",
|
||||
clientSecret: intent.clientSecret ?? "",
|
||||
checkoutUrl: intent.checkoutUrl ?? "",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
export async function signSandboxWebhook(payload: unknown, secret = "sandbox-secret") {
|
||||
const body = JSON.stringify(payload);
|
||||
return { body, signature: await hmac(body, secret) };
|
||||
}
|
||||
|
||||
async function jsonRequest(url: string, init: RequestInit) {
|
||||
const response = await fetch(url, init);
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(`payment gateway request failed (${response.status})`);
|
||||
return data as any;
|
||||
}
|
||||
export interface StripeConfig {
|
||||
secretKey: string;
|
||||
publishableKey: string;
|
||||
webhookSecret: string;
|
||||
apiVersion?: string;
|
||||
}
|
||||
export function stripeGateway(config: StripeConfig): PaymentGateway<StripeConfig> {
|
||||
const auth = {
|
||||
authorization: `Bearer ${config.secretKey}`,
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
"stripe-version": config.apiVersion ?? "2026-03-31",
|
||||
};
|
||||
const form = (data: Record<string, unknown>) =>
|
||||
new URLSearchParams(
|
||||
Object.entries(data)
|
||||
.filter(([, v]) => v !== undefined)
|
||||
.map(([k, v]) => [k, String(v)]),
|
||||
);
|
||||
return defineGateway({
|
||||
id: "stripe",
|
||||
config,
|
||||
capabilities: {
|
||||
...CAPABILITIES,
|
||||
authorizeThenCapture: true,
|
||||
storedMethods: true,
|
||||
customerVault: true,
|
||||
hostedFields: true,
|
||||
disputes: true,
|
||||
},
|
||||
supports: { currencies: "any", countries: "any" },
|
||||
async createIntent(input) {
|
||||
const row = await jsonRequest("https://api.stripe.com/v1/payment_intents", {
|
||||
method: "POST",
|
||||
headers: { ...auth, "idempotency-key": input.idempotencyKey },
|
||||
body: form({
|
||||
amount: input.amount,
|
||||
currency: input.currency.toLowerCase(),
|
||||
capture_method: input.capture === "manual" ? "manual" : "automatic",
|
||||
"metadata[wrnexus_id]": input.id,
|
||||
}),
|
||||
});
|
||||
return {
|
||||
ref: row.id,
|
||||
status: statusOf(row.status),
|
||||
amount: row.amount,
|
||||
currency: String(row.currency).toUpperCase(),
|
||||
clientSecret: row.client_secret,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async fetchIntent(ref) {
|
||||
const row = await jsonRequest(
|
||||
`https://api.stripe.com/v1/payment_intents/${encodeURIComponent(ref)}`,
|
||||
{ headers: auth },
|
||||
);
|
||||
return {
|
||||
ref: row.id,
|
||||
status: statusOf(row.status),
|
||||
amount: row.amount,
|
||||
currency: String(row.currency).toUpperCase(),
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async cancelIntent(ref, reason) {
|
||||
const row = await jsonRequest(
|
||||
`https://api.stripe.com/v1/payment_intents/${encodeURIComponent(ref)}/cancel`,
|
||||
{ method: "POST", headers: auth, body: form({ cancellation_reason: reason }) },
|
||||
);
|
||||
return {
|
||||
ref: row.id,
|
||||
status: "cancelled",
|
||||
amount: row.amount,
|
||||
currency: String(row.currency).toUpperCase(),
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async capture(ref, amount) {
|
||||
const row = await jsonRequest(
|
||||
`https://api.stripe.com/v1/payment_intents/${encodeURIComponent(ref)}/capture`,
|
||||
{ method: "POST", headers: auth, body: form({ amount_to_capture: amount?.amount }) },
|
||||
);
|
||||
return {
|
||||
ref: row.id,
|
||||
status: statusOf(row.status),
|
||||
amount: row.amount_received,
|
||||
currency: String(row.currency).toUpperCase(),
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async refund(input) {
|
||||
const row = await jsonRequest("https://api.stripe.com/v1/refunds", {
|
||||
method: "POST",
|
||||
headers: { ...auth, "idempotency-key": input.idempotencyKey },
|
||||
body: form({
|
||||
payment_intent: input.intentRef,
|
||||
amount: input.amount,
|
||||
reason: "requested_by_customer",
|
||||
}),
|
||||
});
|
||||
return {
|
||||
ref: row.id,
|
||||
status: row.status === "succeeded" ? "succeeded" : "pending",
|
||||
amount: row.amount,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async verifyWebhook(req, secret) {
|
||||
const body = await req.text();
|
||||
const header = req.headers.get("stripe-signature") ?? "";
|
||||
const timestamp = /t=([^,]+)/.exec(header)?.[1] ?? "";
|
||||
const signature = /v1=([^,]+)/.exec(header)?.[1] ?? "";
|
||||
if (!timestamp || !equal(signature, await hmac(`${timestamp}.${body}`, secret)))
|
||||
throw new Error("invalid Stripe signature");
|
||||
const row = JSON.parse(body),
|
||||
object = row.data.object;
|
||||
return {
|
||||
id: row.id,
|
||||
intentRef: object.payment_intent ?? object.id,
|
||||
type: row.type,
|
||||
status: statusOf(object.status),
|
||||
occurredAt: Number(row.created ?? timestamp) * 1000,
|
||||
payload: row,
|
||||
};
|
||||
},
|
||||
async listMethods(customerRef) {
|
||||
const row = await jsonRequest(
|
||||
`https://api.stripe.com/v1/payment_methods?customer=${encodeURIComponent(customerRef)}&type=card`,
|
||||
{ headers: auth },
|
||||
);
|
||||
return (row.data ?? []).map((method: any) => ({
|
||||
ref: method.id,
|
||||
brand: method.card?.brand,
|
||||
last4: method.card?.last4,
|
||||
expires: method.card ? `${method.card.exp_month}/${method.card.exp_year}` : undefined,
|
||||
}));
|
||||
},
|
||||
async attachMethod(customerRef, token) {
|
||||
const row = await jsonRequest(
|
||||
`https://api.stripe.com/v1/payment_methods/${encodeURIComponent(token)}/attach`,
|
||||
{ method: "POST", headers: auth, body: form({ customer: customerRef }) },
|
||||
);
|
||||
return {
|
||||
ref: row.id,
|
||||
brand: row.card?.brand,
|
||||
last4: row.card?.last4,
|
||||
expires: row.card ? `${row.card.exp_month}/${row.card.exp_year}` : undefined,
|
||||
};
|
||||
},
|
||||
async detachMethod(methodRef) {
|
||||
await jsonRequest(
|
||||
`https://api.stripe.com/v1/payment_methods/${encodeURIComponent(methodRef)}/detach`,
|
||||
{ method: "POST", headers: auth },
|
||||
);
|
||||
},
|
||||
async createCustomer(input) {
|
||||
const row = await jsonRequest("https://api.stripe.com/v1/customers", {
|
||||
method: "POST",
|
||||
headers: auth,
|
||||
body: form({ email: input.email, name: input.name, "metadata[subject]": input.subject }),
|
||||
});
|
||||
return row.id;
|
||||
},
|
||||
clientConfig(intent) {
|
||||
return {
|
||||
gateway: "stripe",
|
||||
publishableKey: config.publishableKey,
|
||||
clientSecret: intent.clientSecret ?? "",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
export interface RazorpayConfig {
|
||||
keyId: string;
|
||||
keySecret: string;
|
||||
webhookSecret: string;
|
||||
theme?: { color?: string };
|
||||
}
|
||||
export function razorpayGateway(config: RazorpayConfig): PaymentGateway<RazorpayConfig> {
|
||||
const auth = `Basic ${btoa(`${config.keyId}:${config.keySecret}`)}`;
|
||||
const request = (path: string, init: RequestInit = {}) =>
|
||||
jsonRequest(`https://api.razorpay.com/v1${path}`, {
|
||||
...init,
|
||||
headers: { authorization: auth, "content-type": "application/json", ...(init.headers ?? {}) },
|
||||
});
|
||||
return defineGateway({
|
||||
id: "razorpay",
|
||||
config,
|
||||
capabilities: {
|
||||
...CAPABILITIES,
|
||||
authorizeThenCapture: false,
|
||||
storedMethods: false,
|
||||
customerVault: false,
|
||||
},
|
||||
supports: { currencies: ["INR"], countries: ["IN"] },
|
||||
async createIntent(input) {
|
||||
const row = await request("/orders", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
amount: input.amount,
|
||||
currency: input.currency,
|
||||
receipt: input.id,
|
||||
notes: input.metadata,
|
||||
}),
|
||||
});
|
||||
return {
|
||||
ref: row.id,
|
||||
status: "pending",
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async fetchIntent(ref) {
|
||||
const row = await request(`/orders/${encodeURIComponent(ref)}`);
|
||||
return {
|
||||
ref: row.id,
|
||||
status: statusOf(row.status),
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async cancelIntent(ref) {
|
||||
const row = await this.fetchIntent(ref);
|
||||
return { ...row, status: "cancelled" };
|
||||
},
|
||||
async refund(input) {
|
||||
const row = await request(`/payments/${encodeURIComponent(input.intentRef)}/refund`, {
|
||||
method: "POST",
|
||||
headers: { "X-Razorpay-Idempotency": input.idempotencyKey },
|
||||
body: JSON.stringify({ amount: input.amount, notes: { reason: input.reason } }),
|
||||
});
|
||||
return {
|
||||
ref: row.id,
|
||||
status: row.status === "processed" ? "succeeded" : "pending",
|
||||
amount: row.amount,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async verifyWebhook(req, secret) {
|
||||
const body = await req.text();
|
||||
if (!equal(req.headers.get("x-razorpay-signature") ?? "", await hmac(body, secret)))
|
||||
throw new Error("invalid Razorpay signature");
|
||||
const row = JSON.parse(body),
|
||||
payment = row.payload?.payment?.entity ?? row.payload?.order?.entity;
|
||||
return {
|
||||
id: row.id ?? `${row.event}:${payment.id}`,
|
||||
intentRef: payment.order_id ?? payment.id,
|
||||
type: row.event,
|
||||
status: statusOf(payment.status),
|
||||
occurredAt:
|
||||
Number(payment.created_at ?? row.created_at ?? Math.floor(Date.now() / 1000)) * 1000,
|
||||
payload: row,
|
||||
};
|
||||
},
|
||||
clientConfig(intent) {
|
||||
return {
|
||||
gateway: "razorpay",
|
||||
keyId: config.keyId,
|
||||
orderId: intent.ref,
|
||||
themeColor: config.theme?.color ?? "",
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export interface PayPalConfig {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
webhookId: string;
|
||||
environment?: "sandbox" | "live";
|
||||
}
|
||||
export function paypalGateway(config: PayPalConfig): PaymentGateway<PayPalConfig> {
|
||||
const base =
|
||||
config.environment === "live" ? "https://api-m.paypal.com" : "https://api-m.sandbox.paypal.com";
|
||||
let token = "";
|
||||
async function headers() {
|
||||
if (!token) {
|
||||
const row = await jsonRequest(`${base}/v1/oauth2/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Basic ${btoa(`${config.clientId}:${config.clientSecret}`)}`,
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: "grant_type=client_credentials",
|
||||
});
|
||||
token = row.access_token;
|
||||
}
|
||||
return { authorization: `Bearer ${token}`, "content-type": "application/json" };
|
||||
}
|
||||
const request = async (path: string, init: RequestInit = {}) =>
|
||||
jsonRequest(`${base}${path}`, {
|
||||
...init,
|
||||
headers: { ...(await headers()), ...(init.headers ?? {}) },
|
||||
});
|
||||
return defineGateway({
|
||||
id: "paypal",
|
||||
config,
|
||||
capabilities: { ...CAPABILITIES, partialRefund: true, multipleRefunds: true },
|
||||
supports: { currencies: "any", countries: "any" },
|
||||
async createIntent(input) {
|
||||
const row = await request("/v2/checkout/orders", {
|
||||
method: "POST",
|
||||
headers: { "PayPal-Request-Id": input.idempotencyKey },
|
||||
body: JSON.stringify({
|
||||
intent: "CAPTURE",
|
||||
purchase_units: [
|
||||
{
|
||||
reference_id: input.id,
|
||||
amount: { currency_code: input.currency, value: (input.amount / 100).toFixed(2) },
|
||||
},
|
||||
],
|
||||
application_context: { return_url: input.returnUrl },
|
||||
}),
|
||||
});
|
||||
return {
|
||||
ref: row.id,
|
||||
status: statusOf(String(row.status).toLowerCase()),
|
||||
amount: input.amount,
|
||||
currency: input.currency,
|
||||
checkoutUrl: row.links?.find((x: any) => x.rel === "approve")?.href,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async fetchIntent(ref) {
|
||||
const row = await request(`/v2/checkout/orders/${encodeURIComponent(ref)}`);
|
||||
const unit = row.purchase_units?.[0]?.amount;
|
||||
return {
|
||||
ref: row.id,
|
||||
status: statusOf(String(row.status).toLowerCase()),
|
||||
amount: Math.round(Number(unit?.value ?? 0) * 100),
|
||||
currency: unit?.currency_code ?? "USD",
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async cancelIntent(ref) {
|
||||
const row = await this.fetchIntent(ref);
|
||||
return { ...row, status: "cancelled" };
|
||||
},
|
||||
async refund(input) {
|
||||
const row = await request(
|
||||
`/v2/payments/captures/${encodeURIComponent(input.intentRef)}/refund`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "PayPal-Request-Id": input.idempotencyKey },
|
||||
body: JSON.stringify({
|
||||
amount: { value: (input.amount / 100).toFixed(2), currency_code: input.currency },
|
||||
note_to_payer: input.reason,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return {
|
||||
ref: row.id,
|
||||
status: String(row.status).toUpperCase() === "COMPLETED" ? "succeeded" : "pending",
|
||||
amount: input.amount,
|
||||
raw: row,
|
||||
};
|
||||
},
|
||||
async verifyWebhook(req) {
|
||||
const row = await req.json();
|
||||
const verification = await request("/v1/notifications/verify-webhook-signature", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
auth_algo: req.headers.get("paypal-auth-algo"),
|
||||
cert_url: req.headers.get("paypal-cert-url"),
|
||||
transmission_id: req.headers.get("paypal-transmission-id"),
|
||||
transmission_sig: req.headers.get("paypal-transmission-sig"),
|
||||
transmission_time: req.headers.get("paypal-transmission-time"),
|
||||
webhook_id: config.webhookId,
|
||||
webhook_event: row,
|
||||
}),
|
||||
});
|
||||
if (verification.verification_status !== "SUCCESS")
|
||||
throw new Error("invalid PayPal signature");
|
||||
return {
|
||||
id: row.id,
|
||||
intentRef: row.resource?.supplementary_data?.related_ids?.order_id ?? row.resource?.id,
|
||||
type: row.event_type,
|
||||
status: statusOf(String(row.resource?.status ?? "").toLowerCase()),
|
||||
occurredAt: Date.parse(row.create_time ?? "") || Date.now(),
|
||||
payload: row,
|
||||
};
|
||||
},
|
||||
clientConfig(intent) {
|
||||
return { gateway: "paypal", clientId: config.clientId, orderId: intent.ref };
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import type { CreateIntentInput, GatewayIntent, PaymentGateway } from "./types.ts";
|
||||
|
||||
export interface GatewayContractReport {
|
||||
ok: boolean;
|
||||
issues: string[];
|
||||
}
|
||||
/** Structural half of the shared adapter suite, usable by third-party adapters in CI. */
|
||||
export function verifyGatewayContract(adapter: PaymentGateway): GatewayContractReport {
|
||||
const issues: string[] = [];
|
||||
const capability = (name: string, declared: boolean, present: boolean) => {
|
||||
if (declared !== present)
|
||||
issues.push(
|
||||
`${adapter.id}: capability '${name}' is ${declared ? "declared but not implemented" : "implemented but not declared"}`,
|
||||
);
|
||||
};
|
||||
capability(
|
||||
"authorizeThenCapture",
|
||||
adapter.capabilities.authorizeThenCapture,
|
||||
typeof adapter.capture === "function",
|
||||
);
|
||||
capability(
|
||||
"storedMethods",
|
||||
adapter.capabilities.storedMethods,
|
||||
typeof adapter.listMethods === "function" &&
|
||||
typeof adapter.attachMethod === "function" &&
|
||||
typeof adapter.detachMethod === "function",
|
||||
);
|
||||
capability(
|
||||
"customerVault",
|
||||
adapter.capabilities.customerVault,
|
||||
typeof adapter.createCustomer === "function",
|
||||
);
|
||||
if (!adapter.capabilities.hostedFields && !adapter.capabilities.hostedCheckout)
|
||||
issues.push(`${adapter.id}: a hosted payment surface is required`);
|
||||
if (!adapter.capabilities.webhookSignature)
|
||||
issues.push(`${adapter.id}: signed webhooks are required`);
|
||||
return { ok: issues.length === 0, issues };
|
||||
}
|
||||
|
||||
export interface GatewayContractFixture {
|
||||
input: CreateIntentInput;
|
||||
webhook(intent: GatewayIntent): Request | Promise<Request>;
|
||||
}
|
||||
|
||||
/** Executable lifecycle contract shared by built-in and third-party adapters. */
|
||||
export async function exerciseGatewayContract(
|
||||
adapter: PaymentGateway,
|
||||
fixture: GatewayContractFixture,
|
||||
): Promise<GatewayContractReport> {
|
||||
const report = verifyGatewayContract(adapter);
|
||||
const issues = [...report.issues];
|
||||
const check = (condition: unknown, message: string) => {
|
||||
if (!condition) issues.push(`${adapter.id}: ${message}`);
|
||||
};
|
||||
try {
|
||||
const intent = await adapter.createIntent(fixture.input);
|
||||
check(Boolean(intent.ref), "createIntent returned no reference");
|
||||
check(intent.amount === fixture.input.amount, "createIntent changed the amount");
|
||||
check(intent.currency === fixture.input.currency, "createIntent changed the currency");
|
||||
const duplicate = await adapter.createIntent(fixture.input);
|
||||
check(duplicate.ref === intent.ref, "createIntent did not honor its idempotency key");
|
||||
const fetched = await adapter.fetchIntent(intent.ref);
|
||||
check(fetched.ref === intent.ref, "fetchIntent returned a different reference");
|
||||
const client = adapter.clientConfig(intent);
|
||||
const configuredSecrets = Object.entries((adapter.config ?? {}) as Record<string, unknown>)
|
||||
.filter(([key, value]) => /secret|password/i.test(key) && typeof value === "string")
|
||||
.map(([, value]) => value);
|
||||
check(
|
||||
!configuredSecrets.some((secret) => Object.values(client).includes(secret as string)),
|
||||
"clientConfig exposed a configured secret",
|
||||
);
|
||||
if (adapter.capabilities.authorizeThenCapture) {
|
||||
const captured = await adapter.capture!(intent.ref);
|
||||
check(Boolean(captured.ref), "capture returned no reference");
|
||||
}
|
||||
const cancellable = await adapter.createIntent({
|
||||
...fixture.input,
|
||||
id: `${fixture.input.id}-cancel`,
|
||||
idempotencyKey: `${fixture.input.idempotencyKey}:cancel`,
|
||||
});
|
||||
const cancelled = await adapter.cancelIntent(cancellable.ref, "contract");
|
||||
check(cancelled.status === "cancelled", "cancelIntent did not return cancelled state");
|
||||
const refund = await adapter.refund({
|
||||
intentRef: intent.ref,
|
||||
amount: Math.max(1, Math.floor(intent.amount / 2)),
|
||||
currency: intent.currency,
|
||||
reason: "contract",
|
||||
idempotencyKey: `${fixture.input.idempotencyKey}:refund`,
|
||||
});
|
||||
check(Boolean(refund.ref), "refund returned no reference");
|
||||
if (adapter.capabilities.customerVault) {
|
||||
const customer = await adapter.createCustomer!({ subject: fixture.input.subject });
|
||||
check(Boolean(customer), "createCustomer returned no reference");
|
||||
}
|
||||
if (adapter.capabilities.storedMethods) {
|
||||
const customer = "contract-customer";
|
||||
const method = await adapter.attachMethod!(customer, "contract-token-4242");
|
||||
check(Boolean(method.ref), "attachMethod returned no reference");
|
||||
check(
|
||||
Array.isArray(await adapter.listMethods!(customer)),
|
||||
"listMethods did not return an array",
|
||||
);
|
||||
await adapter.detachMethod!(method.ref);
|
||||
}
|
||||
const event = await adapter.verifyWebhook(
|
||||
await fixture.webhook(intent),
|
||||
"contract-webhook-secret",
|
||||
);
|
||||
check(Boolean(event.id), "verified webhook has no gateway event id");
|
||||
check(Boolean(event.intentRef), "verified webhook has no intent reference");
|
||||
check(Number.isFinite(event.occurredAt), "verified webhook has no gateway occurrence time");
|
||||
} catch (error) {
|
||||
issues.push(
|
||||
`${adapter.id}: lifecycle threw ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
return { ok: issues.length === 0, issues };
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
import type {
|
||||
GatewayMethod,
|
||||
Money,
|
||||
PaymentEvent,
|
||||
PaymentGateway,
|
||||
PaymentIntent,
|
||||
PaymentMethod,
|
||||
PaymentRefund,
|
||||
PaymentResult,
|
||||
PaymentStatus,
|
||||
PaymentStore,
|
||||
WebhookEvent,
|
||||
} from "./types.ts";
|
||||
|
||||
export interface InitializePaymentInput extends Money {
|
||||
subject: string;
|
||||
idempotencyKey: string;
|
||||
gateway?: string;
|
||||
country?: string;
|
||||
capture?: "automatic" | "manual";
|
||||
metadata?: Record<string, string>;
|
||||
returnUrl?: string;
|
||||
}
|
||||
export interface PaymentOptions {
|
||||
store: PaymentStore;
|
||||
adapters: readonly PaymentGateway[];
|
||||
default: string;
|
||||
route?(input: InitializePaymentInput): string;
|
||||
webhookSecrets?: Record<string, string>;
|
||||
environment?: "development" | "test" | "production";
|
||||
}
|
||||
|
||||
const validMoney = (money: Money) =>
|
||||
Number.isSafeInteger(money.amount) && money.amount > 0 && /^[A-Z]{3}$/.test(money.currency);
|
||||
const refusal = <T>(reason: string): PaymentResult<T> => ({ ok: false, fault: "refused", reason });
|
||||
const gatewayFault = <T>(reason: string): PaymentResult<T> => ({
|
||||
ok: false,
|
||||
fault: "gateway",
|
||||
reason,
|
||||
});
|
||||
export function derivePaymentStatus(events: readonly PaymentEvent[]): PaymentStatus {
|
||||
const ordered = [...events].sort((left, right) => {
|
||||
if (left.sequence !== undefined && right.sequence !== undefined) {
|
||||
const numeric = Number(left.sequence) - Number(right.sequence);
|
||||
if (Number.isFinite(numeric) && numeric !== 0) return numeric;
|
||||
const lexical = String(left.sequence).localeCompare(String(right.sequence));
|
||||
if (lexical) return lexical;
|
||||
}
|
||||
return (
|
||||
(left.occurredAt ?? left.at) - (right.occurredAt ?? right.at) ||
|
||||
left.id.localeCompare(right.id)
|
||||
);
|
||||
});
|
||||
let status: PaymentStatus = "created";
|
||||
for (const event of ordered) {
|
||||
if (status === "refunded") continue;
|
||||
if (
|
||||
status === "partially_refunded" &&
|
||||
!["partially_refunded", "refunded"].includes(event.status)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
status === "captured" &&
|
||||
["created", "pending", "authorized", "failed", "cancelled"].includes(event.status)
|
||||
)
|
||||
continue;
|
||||
if (status === "authorized" && ["created", "pending"].includes(event.status)) continue;
|
||||
status = event.status;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
export function defineGateway<T extends PaymentGateway>(gateway: T): T {
|
||||
if (!gateway.id.trim()) throw new Error("WRN-PAYMENT-GATEWAY: id is required");
|
||||
if (!gateway.capabilities.webhookSignature && process.env.NODE_ENV === "production")
|
||||
throw new Error(`WRN-PAYMENT-WEBHOOK: gateway '${gateway.id}' cannot verify signatures`);
|
||||
return Object.freeze(gateway);
|
||||
}
|
||||
|
||||
export function definePayment(options: PaymentOptions) {
|
||||
const adapters = new Map(options.adapters.map((adapter) => [adapter.id, adapter]));
|
||||
if (adapters.size !== options.adapters.length)
|
||||
throw new Error("WRN-PAYMENT-CONFIG: duplicate gateway id");
|
||||
if (!adapters.has(options.default))
|
||||
throw new Error(`WRN-PAYMENT-CONFIG: default gateway '${options.default}' is not registered`);
|
||||
if ((options.environment ?? process.env.NODE_ENV) === "production")
|
||||
for (const adapter of adapters.values())
|
||||
if (!adapter.capabilities.webhookSignature)
|
||||
throw new Error(`WRN-PAYMENT-WEBHOOK: gateway '${adapter.id}' cannot run in production`);
|
||||
const adapterFor = (id: string) => adapters.get(id);
|
||||
const status = async (id: string) => derivePaymentStatus(await options.store.events(id));
|
||||
|
||||
async function initializePayment(
|
||||
input: InitializePaymentInput,
|
||||
): Promise<PaymentResult<PaymentIntent>> {
|
||||
if (!validMoney(input))
|
||||
return refusal("amount must be positive minor units and currency must be ISO-4217");
|
||||
if (!input.subject.trim() || !input.idempotencyKey.trim())
|
||||
return refusal("subject and idempotencyKey are required");
|
||||
try {
|
||||
const existing = await options.store.findIntentByKey(input.idempotencyKey);
|
||||
if (existing) return { ok: true, value: existing };
|
||||
const gatewayId = input.gateway ?? options.route?.(input) ?? options.default;
|
||||
const adapter = adapterFor(gatewayId);
|
||||
if (!adapter) return refusal(`gateway '${gatewayId}' is not configured`);
|
||||
if (
|
||||
adapter.supports.currencies !== "any" &&
|
||||
!adapter.supports.currencies.includes(input.currency)
|
||||
)
|
||||
return refusal(`gateway '${gatewayId}' does not support ${input.currency}`);
|
||||
if (
|
||||
input.country &&
|
||||
adapter.supports.countries !== "any" &&
|
||||
!adapter.supports.countries.includes(input.country)
|
||||
)
|
||||
return refusal(`gateway '${gatewayId}' does not support country ${input.country}`);
|
||||
if (input.capture === "manual" && !adapter.capabilities.authorizeThenCapture)
|
||||
return refusal(`gateway '${gatewayId}' does not support authorize-then-capture`);
|
||||
const id = `pay_${crypto.randomUUID()}`;
|
||||
const remote = await adapter.createIntent({ ...input, id });
|
||||
const intent: PaymentIntent = {
|
||||
id,
|
||||
subject: input.subject,
|
||||
amount: input.amount,
|
||||
currency: input.currency,
|
||||
gateway: gatewayId,
|
||||
gatewayRef: remote.ref,
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
createdAt: Date.now(),
|
||||
checkoutUrl: remote.checkoutUrl,
|
||||
client: adapter.clientConfig(remote),
|
||||
metadata: { ...(input.metadata ?? {}) },
|
||||
};
|
||||
await options.store.putIntent(intent);
|
||||
await options.store.appendEvent({
|
||||
id: `local:${id}:created`,
|
||||
intentId: id,
|
||||
type: "payment.created",
|
||||
status: remote.status,
|
||||
at: Date.now(),
|
||||
});
|
||||
return { ok: true, value: intent };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "gateway failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function getPayment(id: string) {
|
||||
return options.store.getIntent(id);
|
||||
}
|
||||
async function checkPayment(id: string): Promise<PaymentStatus> {
|
||||
return status(id);
|
||||
}
|
||||
async function syncPayment(id: string): Promise<PaymentResult<PaymentIntent>> {
|
||||
try {
|
||||
const intent = await options.store.getIntent(id);
|
||||
if (!intent) return refusal("payment not found");
|
||||
const remote = await adapterFor(intent.gateway)!.fetchIntent(intent.gatewayRef);
|
||||
await options.store.appendEvent({
|
||||
id: `sync:${intent.gateway}:${remote.ref}:${remote.status}`,
|
||||
intentId: id,
|
||||
type: "payment.synced",
|
||||
status: remote.status,
|
||||
at: Date.now(),
|
||||
payload: remote.raw,
|
||||
});
|
||||
return { ok: true, value: intent };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "sync failed");
|
||||
}
|
||||
}
|
||||
const confirmPayment = syncPayment;
|
||||
async function capturePayment(id: string, amount?: Money): Promise<PaymentResult<PaymentIntent>> {
|
||||
try {
|
||||
const intent = await options.store.getIntent(id);
|
||||
if (!intent) return refusal("payment not found");
|
||||
const adapter = adapterFor(intent.gateway)!;
|
||||
if (!adapter.capabilities.authorizeThenCapture || !adapter.capture)
|
||||
return refusal(`gateway '${adapter.id}' does not support capture`);
|
||||
const remote = await adapter.capture(intent.gatewayRef, amount);
|
||||
await options.store.appendEvent({
|
||||
id: `capture:${remote.ref}:${Date.now()}`,
|
||||
intentId: id,
|
||||
type: "payment.captured",
|
||||
status: remote.status,
|
||||
at: Date.now(),
|
||||
payload: remote.raw,
|
||||
});
|
||||
return { ok: true, value: intent };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "capture failed");
|
||||
}
|
||||
}
|
||||
async function cancelPayment(id: string, reason: string): Promise<PaymentResult<PaymentIntent>> {
|
||||
try {
|
||||
const intent = await options.store.getIntent(id);
|
||||
if (!intent) return refusal("payment not found");
|
||||
if (!reason.trim()) return refusal("cancellation reason is required");
|
||||
const remote = await adapterFor(intent.gateway)!.cancelIntent(intent.gatewayRef, reason);
|
||||
await options.store.appendEvent({
|
||||
id: `cancel:${remote.ref}:${Date.now()}`,
|
||||
intentId: id,
|
||||
type: "payment.cancelled",
|
||||
status: remote.status,
|
||||
at: Date.now(),
|
||||
payload: remote.raw,
|
||||
});
|
||||
return { ok: true, value: intent };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "cancellation failed");
|
||||
}
|
||||
}
|
||||
async function refundableAmount(id: string): Promise<Money> {
|
||||
const intent = await options.store.getIntent(id);
|
||||
if (!intent) return { amount: 0, currency: "XXX" };
|
||||
const captured =
|
||||
(await status(id)) === "captured" ||
|
||||
(await status(id)) === "partially_refunded" ||
|
||||
(await status(id)) === "refunded"
|
||||
? intent.amount
|
||||
: 0;
|
||||
const refunded = (await options.store.refunds(id))
|
||||
.filter((row) => row.status !== "failed")
|
||||
.reduce((sum, row) => sum + row.amount, 0);
|
||||
return { amount: Math.max(0, captured - refunded), currency: intent.currency };
|
||||
}
|
||||
async function refundPayment(input: {
|
||||
id: string;
|
||||
amount?: number;
|
||||
reason: string;
|
||||
idempotencyKey: string;
|
||||
}): Promise<PaymentResult<PaymentRefund>> {
|
||||
try {
|
||||
const intent = await options.store.getIntent(input.id);
|
||||
if (!intent) return refusal("payment not found");
|
||||
if (!input.reason.trim() || !input.idempotencyKey.trim())
|
||||
return refusal("refund reason and idempotencyKey are required");
|
||||
const prior = (await options.store.refunds(input.id)).find(
|
||||
(row) => row.idempotencyKey === input.idempotencyKey,
|
||||
);
|
||||
if (prior) return { ok: true, value: prior };
|
||||
const available = await refundableAmount(input.id);
|
||||
const amount = input.amount ?? available.amount;
|
||||
if (!Number.isSafeInteger(amount) || amount <= 0 || amount > available.amount)
|
||||
return refusal("refund exceeds the refundable amount");
|
||||
const adapter = adapterFor(intent.gateway)!;
|
||||
if (amount < intent.amount && !adapter.capabilities.partialRefund)
|
||||
return refusal(`gateway '${adapter.id}' does not support partial refunds`);
|
||||
if (
|
||||
(await options.store.refunds(input.id)).some((row) => row.status !== "failed") &&
|
||||
!adapter.capabilities.multipleRefunds
|
||||
)
|
||||
return refusal(`gateway '${adapter.id}' does not support multiple refunds`);
|
||||
const refund: PaymentRefund = {
|
||||
id: `refund_${crypto.randomUUID()}`,
|
||||
intentId: intent.id,
|
||||
gatewayRef: "",
|
||||
amount,
|
||||
currency: intent.currency,
|
||||
reason: input.reason,
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
status: "pending",
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
if (!(await options.store.reserveRefund(refund, intent.amount)))
|
||||
return refusal("refund is duplicated or exceeds the captured amount");
|
||||
const remote = await adapter.refund({
|
||||
intentRef: intent.gatewayRef,
|
||||
amount,
|
||||
currency: intent.currency,
|
||||
reason: input.reason,
|
||||
idempotencyKey: input.idempotencyKey,
|
||||
});
|
||||
await options.store.updateRefund(refund.id, remote.status, remote.ref);
|
||||
const completed = { ...refund, status: remote.status, gatewayRef: remote.ref };
|
||||
const remaining = available.amount - amount;
|
||||
await options.store.appendEvent({
|
||||
id: `refund:${remote.ref}`,
|
||||
intentId: intent.id,
|
||||
type: "payment.refunded",
|
||||
status: remaining === 0 ? "refunded" : "partially_refunded",
|
||||
at: Date.now(),
|
||||
payload: remote.raw,
|
||||
});
|
||||
return { ok: true, value: completed };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "refund failed");
|
||||
}
|
||||
}
|
||||
async function acceptWebhook(gatewayId: string, event: WebhookEvent): Promise<boolean> {
|
||||
const intent = (await options.store.listIntents({ gateway: gatewayId })).find(
|
||||
(row) => row.gatewayRef === event.intentRef,
|
||||
);
|
||||
if (!intent) return false;
|
||||
return options.store.appendEvent({
|
||||
id: `${gatewayId}:${event.id}`,
|
||||
intentId: intent.id,
|
||||
type: event.type,
|
||||
status: event.status,
|
||||
at: Date.now(),
|
||||
occurredAt: event.occurredAt,
|
||||
sequence: event.sequence,
|
||||
payload: event.payload,
|
||||
});
|
||||
}
|
||||
function paymentWebhookHandler(gatewayId: string) {
|
||||
return async (ctx: { req: Request }) => {
|
||||
const adapter = adapterFor(gatewayId);
|
||||
const secret = options.webhookSecrets?.[gatewayId];
|
||||
if (!adapter || !secret)
|
||||
return Response.json({ error: "gateway webhook is not configured" }, { status: 404 });
|
||||
try {
|
||||
const event = await adapter.verifyWebhook(ctx.req, secret);
|
||||
const accepted = await acceptWebhook(gatewayId, event);
|
||||
return Response.json({ received: true, duplicate: !accepted });
|
||||
} catch {
|
||||
return Response.json({ error: "invalid webhook" }, { status: 400 });
|
||||
}
|
||||
};
|
||||
}
|
||||
async function paymentMethods(subject: string): Promise<PaymentMethod[]> {
|
||||
return options.store.methods(subject);
|
||||
}
|
||||
async function attachPaymentMethod(
|
||||
subject: string,
|
||||
gatewayId: string,
|
||||
customerRef: string,
|
||||
token: string,
|
||||
): Promise<PaymentResult<PaymentMethod>> {
|
||||
try {
|
||||
const adapter = adapterFor(gatewayId);
|
||||
if (!adapter?.capabilities.storedMethods || !adapter.attachMethod)
|
||||
return refusal(`gateway '${gatewayId}' does not support stored methods`);
|
||||
const remote: GatewayMethod = await adapter.attachMethod(customerRef, token);
|
||||
const method: PaymentMethod = {
|
||||
id: `method_${crypto.randomUUID()}`,
|
||||
subject,
|
||||
gateway: gatewayId,
|
||||
gatewayRef: remote.ref,
|
||||
brand: remote.brand,
|
||||
last4: remote.last4,
|
||||
expires: remote.expires,
|
||||
default: false,
|
||||
};
|
||||
await options.store.putMethod(method);
|
||||
return { ok: true, value: method };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "method attach failed");
|
||||
}
|
||||
}
|
||||
async function detachPaymentMethod(id: string): Promise<PaymentResult<void>> {
|
||||
const method = await options.store.getMethod(id);
|
||||
if (!method) return refusal("payment method not found");
|
||||
const adapter = adapterFor(method.gateway);
|
||||
try {
|
||||
await adapter?.detachMethod?.(method.gatewayRef);
|
||||
await options.store.removeMethod(id);
|
||||
return { ok: true, value: undefined };
|
||||
} catch (error) {
|
||||
return gatewayFault(error instanceof Error ? error.message : "method detach failed");
|
||||
}
|
||||
}
|
||||
async function setDefaultPaymentMethod(subject: string, id: string) {
|
||||
await options.store.defaultMethod(subject, id);
|
||||
}
|
||||
async function listPayments(filter?: { subject?: string; gateway?: string }) {
|
||||
return options.store.listIntents(filter);
|
||||
}
|
||||
async function listRefunds(id: string) {
|
||||
return options.store.refunds(id);
|
||||
}
|
||||
async function paymentTotals(filter?: { subject?: string; gateway?: string }) {
|
||||
const rows = await options.store.listIntents(filter);
|
||||
const byCurrency: Record<string, { captured: number; refunded: number; net: number }> = {};
|
||||
for (const row of rows) {
|
||||
const state = await status(row.id);
|
||||
const captured = ["captured", "partially_refunded", "refunded"].includes(state)
|
||||
? row.amount
|
||||
: 0;
|
||||
const refunded = (await options.store.refunds(row.id))
|
||||
.filter((x) => x.status === "succeeded")
|
||||
.reduce((s, x) => s + x.amount, 0);
|
||||
const total = (byCurrency[row.currency] ??= { captured: 0, refunded: 0, net: 0 });
|
||||
total.captured += captured;
|
||||
total.refunded += refunded;
|
||||
total.net = total.captured - total.refunded;
|
||||
}
|
||||
return { byCurrency };
|
||||
}
|
||||
async function reconcilePayments() {
|
||||
const discrepancies: unknown[] = [];
|
||||
for (const intent of await options.store.listIntents()) {
|
||||
const local = await status(intent.id);
|
||||
const remote = (await adapterFor(intent.gateway)!.fetchIntent(intent.gatewayRef)).status;
|
||||
if (local !== remote)
|
||||
discrepancies.push({ id: intent.id, local, remote, gateway: intent.gateway });
|
||||
}
|
||||
return discrepancies;
|
||||
}
|
||||
return {
|
||||
initializePayment,
|
||||
getPayment,
|
||||
listPayments,
|
||||
checkPayment,
|
||||
confirmPayment,
|
||||
syncPayment,
|
||||
capturePayment,
|
||||
cancelPayment,
|
||||
refundPayment,
|
||||
listRefunds,
|
||||
refundableAmount,
|
||||
paymentMethods,
|
||||
attachPaymentMethod,
|
||||
detachPaymentMethod,
|
||||
setDefaultPaymentMethod,
|
||||
paymentTotals,
|
||||
reconcilePayments,
|
||||
paymentWebhookHandler,
|
||||
capabilitiesOf: (id: string) => adapterFor(id)?.capabilities,
|
||||
};
|
||||
}
|
||||
|
||||
export type PaymentService = ReturnType<typeof definePayment>;
|
||||
const SERVICE = Symbol.for("@wrnexus/payment:service:v1");
|
||||
type PaymentGlobal = typeof globalThis & { [SERVICE]?: PaymentService };
|
||||
export function configurePayment(options: PaymentOptions): PaymentService {
|
||||
const service = definePayment(options);
|
||||
(globalThis as PaymentGlobal)[SERVICE] = service;
|
||||
return service;
|
||||
}
|
||||
export function getPaymentService(): PaymentService {
|
||||
const service = (globalThis as PaymentGlobal)[SERVICE];
|
||||
if (!service)
|
||||
throw new Error("WRN-PAYMENT-SETUP: call configurePayment() during application startup");
|
||||
return service;
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
import { getDb } from "@wrnexus/db";
|
||||
import type { PaymentIntent, PaymentRefund, PaymentStore } from "./types.ts";
|
||||
|
||||
const parse = <T>(value: unknown, fallback: T): T => {
|
||||
try {
|
||||
return typeof value === "string" ? JSON.parse(value) : (value as T);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
};
|
||||
export function databasePaymentStore(database = "default"): PaymentStore {
|
||||
const db = () => getDb(database);
|
||||
const intent = (row: any): PaymentIntent => ({
|
||||
id: String(row.id),
|
||||
subject: String(row.subject),
|
||||
gateway: String(row.gateway),
|
||||
gatewayRef: String(row.gateway_ref),
|
||||
amount: Number(row.amount),
|
||||
currency: String(row.currency),
|
||||
idempotencyKey: String(row.idempotency_key),
|
||||
createdAt: Number(row.created_at),
|
||||
checkoutUrl: row.checkout_url ? String(row.checkout_url) : undefined,
|
||||
client: parse(row.client_json, {}),
|
||||
metadata: parse(row.metadata_json, {}),
|
||||
});
|
||||
const refund = (row: any): PaymentRefund => ({
|
||||
id: String(row.id),
|
||||
intentId: String(row.intent_id),
|
||||
gatewayRef: String(row.gateway_ref ?? ""),
|
||||
amount: Number(row.amount),
|
||||
currency: String(row.currency),
|
||||
reason: String(row.reason),
|
||||
idempotencyKey: String(row.idempotency_key),
|
||||
status: row.status,
|
||||
createdAt: Number(row.created_at),
|
||||
});
|
||||
return {
|
||||
async findIntentByKey(key) {
|
||||
const row = await db().one("SELECT * FROM wrn_payment_intent WHERE idempotency_key = ?", [
|
||||
key,
|
||||
]);
|
||||
return row ? intent(row) : null;
|
||||
},
|
||||
async putIntent(row) {
|
||||
await db().exec(
|
||||
"INSERT INTO wrn_payment_intent (id,subject,gateway,gateway_ref,amount,currency,idempotency_key,created_at,checkout_url,client_json,metadata_json) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
[
|
||||
row.id,
|
||||
row.subject,
|
||||
row.gateway,
|
||||
row.gatewayRef,
|
||||
row.amount,
|
||||
row.currency,
|
||||
row.idempotencyKey,
|
||||
row.createdAt,
|
||||
row.checkoutUrl ?? null,
|
||||
JSON.stringify(row.client),
|
||||
JSON.stringify(row.metadata),
|
||||
],
|
||||
);
|
||||
},
|
||||
async getIntent(id) {
|
||||
const row = await db().one("SELECT * FROM wrn_payment_intent WHERE id = ?", [id]);
|
||||
return row ? intent(row) : null;
|
||||
},
|
||||
async listIntents(filter = {}) {
|
||||
const clauses: string[] = [],
|
||||
values: unknown[] = [];
|
||||
if (filter.subject) {
|
||||
clauses.push("subject = ?");
|
||||
values.push(filter.subject);
|
||||
}
|
||||
if (filter.gateway) {
|
||||
clauses.push("gateway = ?");
|
||||
values.push(filter.gateway);
|
||||
}
|
||||
return (
|
||||
await db().all(
|
||||
`SELECT * FROM wrn_payment_intent${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at DESC`,
|
||||
values,
|
||||
)
|
||||
).map(intent);
|
||||
},
|
||||
async appendEvent(row) {
|
||||
try {
|
||||
const result = await db().exec(
|
||||
"INSERT INTO wrn_payment_event (id,intent_id,type,status,created_at,occurred_at,gateway_sequence,payload_json) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(id) DO NOTHING",
|
||||
[
|
||||
row.id,
|
||||
row.intentId,
|
||||
row.type,
|
||||
row.status,
|
||||
row.at,
|
||||
row.occurredAt ?? row.at,
|
||||
row.sequence === undefined ? null : String(row.sequence),
|
||||
JSON.stringify(row.payload ?? null),
|
||||
],
|
||||
);
|
||||
return result.changes > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async events(id) {
|
||||
return (
|
||||
await db().all<any>(
|
||||
"SELECT * FROM wrn_payment_event WHERE intent_id = ? ORDER BY occurred_at ASC, gateway_sequence ASC, id ASC",
|
||||
[id],
|
||||
)
|
||||
).map((row) => ({
|
||||
id: String(row.id),
|
||||
intentId: String(row.intent_id),
|
||||
type: String(row.type),
|
||||
status: row.status,
|
||||
at: Number(row.created_at),
|
||||
occurredAt: Number(row.occurred_at ?? row.created_at),
|
||||
sequence: row.gateway_sequence == null ? undefined : String(row.gateway_sequence),
|
||||
payload: parse(row.payload_json, null),
|
||||
}));
|
||||
},
|
||||
async reserveRefund(row, captured) {
|
||||
try {
|
||||
const result = await db().exec(
|
||||
"INSERT INTO wrn_payment_refund (id,intent_id,gateway_ref,amount,currency,reason,idempotency_key,status,created_at) SELECT ?,?,?,?,?,?,?,?,? WHERE ? >= COALESCE((SELECT SUM(amount) FROM wrn_payment_refund WHERE intent_id = ? AND status <> 'failed'),0) + ?",
|
||||
[
|
||||
row.id,
|
||||
row.intentId,
|
||||
row.gatewayRef,
|
||||
row.amount,
|
||||
row.currency,
|
||||
row.reason,
|
||||
row.idempotencyKey,
|
||||
row.status,
|
||||
row.createdAt,
|
||||
captured,
|
||||
row.intentId,
|
||||
row.amount,
|
||||
],
|
||||
);
|
||||
return result.changes > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async updateRefund(id, status, gatewayRef) {
|
||||
await db().exec("UPDATE wrn_payment_refund SET status = ?, gateway_ref = ? WHERE id = ?", [
|
||||
status,
|
||||
gatewayRef,
|
||||
id,
|
||||
]);
|
||||
},
|
||||
async refunds(id) {
|
||||
return (
|
||||
await db().all("SELECT * FROM wrn_payment_refund WHERE intent_id = ? ORDER BY created_at", [
|
||||
id,
|
||||
])
|
||||
).map(refund);
|
||||
},
|
||||
async methods(subject) {
|
||||
return (
|
||||
await db().all<any>(
|
||||
"SELECT * FROM wrn_payment_method WHERE subject = ? ORDER BY is_default DESC, created_at",
|
||||
[subject],
|
||||
)
|
||||
).map((row) => ({
|
||||
id: String(row.id),
|
||||
subject: String(row.subject),
|
||||
gateway: String(row.gateway),
|
||||
gatewayRef: String(row.gateway_ref),
|
||||
brand: row.brand ? String(row.brand) : undefined,
|
||||
last4: row.last4 ? String(row.last4) : undefined,
|
||||
expires: row.expires ? String(row.expires) : undefined,
|
||||
default: Boolean(row.is_default),
|
||||
}));
|
||||
},
|
||||
async getMethod(id) {
|
||||
const row = await db().one<any>("SELECT * FROM wrn_payment_method WHERE id = ?", [id]);
|
||||
return row
|
||||
? {
|
||||
id: String(row.id),
|
||||
subject: String(row.subject),
|
||||
gateway: String(row.gateway),
|
||||
gatewayRef: String(row.gateway_ref),
|
||||
brand: row.brand ? String(row.brand) : undefined,
|
||||
last4: row.last4 ? String(row.last4) : undefined,
|
||||
expires: row.expires ? String(row.expires) : undefined,
|
||||
default: Boolean(row.is_default),
|
||||
}
|
||||
: null;
|
||||
},
|
||||
async putMethod(row) {
|
||||
await db().exec(
|
||||
"INSERT INTO wrn_payment_method (id,subject,gateway,gateway_ref,brand,last4,expires,is_default,created_at) VALUES (?,?,?,?,?,?,?,?,?)",
|
||||
[
|
||||
row.id,
|
||||
row.subject,
|
||||
row.gateway,
|
||||
row.gatewayRef,
|
||||
row.brand ?? null,
|
||||
row.last4 ?? null,
|
||||
row.expires ?? null,
|
||||
row.default ? 1 : 0,
|
||||
Date.now(),
|
||||
],
|
||||
);
|
||||
},
|
||||
async removeMethod(id) {
|
||||
await db().exec("DELETE FROM wrn_payment_method WHERE id = ?", [id]);
|
||||
},
|
||||
async defaultMethod(subject, id) {
|
||||
await db().tx(async (tx) => {
|
||||
await tx.exec("UPDATE wrn_payment_method SET is_default = 0 WHERE subject = ?", [subject]);
|
||||
await tx.exec("UPDATE wrn_payment_method SET is_default = 1 WHERE subject = ? AND id = ?", [
|
||||
subject,
|
||||
id,
|
||||
]);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export * from "./types.ts";
|
||||
export * from "./store.ts";
|
||||
export * from "./database.ts";
|
||||
export * from "./core.ts";
|
||||
export * from "./adapters.ts";
|
||||
export * from "./contract.ts";
|
||||
export { paymentPlugin } from "./plugin.ts";
|
||||
@@ -0,0 +1,65 @@
|
||||
import { basename, dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const extension = basename(dirname(fileURLToPath(import.meta.url))) === "dist" ? ".js" : ".ts";
|
||||
const webhookEntry = join(dirname(fileURLToPath(import.meta.url)), `runtime-webhook${extension}`);
|
||||
const migration = `-- +up
|
||||
CREATE TABLE IF NOT EXISTS wrn_payment_intent (id TEXT PRIMARY KEY, subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, amount BIGINT NOT NULL, currency TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, created_at BIGINT NOT NULL, checkout_url TEXT, client_json TEXT NOT NULL, metadata_json TEXT NOT NULL);
|
||||
CREATE INDEX IF NOT EXISTS wrn_payment_intent_subject ON wrn_payment_intent(subject, created_at);
|
||||
CREATE TABLE IF NOT EXISTS wrn_payment_event (id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, type TEXT NOT NULL, status TEXT NOT NULL, created_at BIGINT NOT NULL, payload_json TEXT, FOREIGN KEY(intent_id) REFERENCES wrn_payment_intent(id));
|
||||
CREATE INDEX IF NOT EXISTS wrn_payment_event_intent ON wrn_payment_event(intent_id, created_at, id);
|
||||
CREATE TABLE IF NOT EXISTS wrn_payment_refund (id TEXT PRIMARY KEY, intent_id TEXT NOT NULL, gateway_ref TEXT NOT NULL, amount BIGINT NOT NULL, currency TEXT NOT NULL, reason TEXT NOT NULL, idempotency_key TEXT NOT NULL UNIQUE, status TEXT NOT NULL, created_at BIGINT NOT NULL, FOREIGN KEY(intent_id) REFERENCES wrn_payment_intent(id));
|
||||
CREATE TABLE IF NOT EXISTS wrn_payment_method (id TEXT PRIMARY KEY, subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, brand TEXT, last4 TEXT, expires TEXT, is_default INTEGER NOT NULL DEFAULT 0, created_at BIGINT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS wrn_payment_customer (subject TEXT NOT NULL, gateway TEXT NOT NULL, gateway_ref TEXT NOT NULL, created_at BIGINT NOT NULL, PRIMARY KEY(subject,gateway));
|
||||
-- +down
|
||||
DROP TABLE IF EXISTS wrn_payment_customer;
|
||||
DROP TABLE IF EXISTS wrn_payment_method;
|
||||
DROP TABLE IF EXISTS wrn_payment_refund;
|
||||
DROP TABLE IF EXISTS wrn_payment_event;
|
||||
DROP TABLE IF EXISTS wrn_payment_intent;`;
|
||||
const orderingMigration = `-- +up
|
||||
ALTER TABLE wrn_payment_event ADD COLUMN occurred_at BIGINT;
|
||||
ALTER TABLE wrn_payment_event ADD COLUMN gateway_sequence TEXT;
|
||||
UPDATE wrn_payment_event SET occurred_at = created_at WHERE occurred_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS wrn_payment_event_gateway_order ON wrn_payment_event(intent_id, occurred_at, gateway_sequence, id);
|
||||
-- +down
|
||||
DROP INDEX IF EXISTS wrn_payment_event_gateway_order;`;
|
||||
|
||||
export function paymentPlugin() {
|
||||
const key = "@wrnexus/payment:enabled";
|
||||
return definePlugin({
|
||||
name: "@wrnexus/payment",
|
||||
version: "0.8.1",
|
||||
componentDirs: [join(root, "components")],
|
||||
routeEntries(context) {
|
||||
return context.metadata.get(key)
|
||||
? [{ kind: "api" as const, path: "/api/payments/webhook/:gateway", entry: webhookEntry }]
|
||||
: [];
|
||||
},
|
||||
configure(config, context) {
|
||||
const value = config.payment as
|
||||
{ default?: string; gateways?: Record<string, unknown>; sandbox?: boolean } | undefined;
|
||||
context.metadata.set(key, Boolean(value));
|
||||
if (!value) return;
|
||||
if (!value.default) throw new Error("WRN-PAYMENT-CONFIG: payment.default is required");
|
||||
if (!value.gateways?.[value.default] && value.default !== "sandbox")
|
||||
throw new Error(
|
||||
`WRN-PAYMENT-CONFIG: default gateway '${value.default}' has no configuration`,
|
||||
);
|
||||
if (process.env.NODE_ENV === "production" && value.sandbox)
|
||||
throw new Error("WRN-PAYMENT-CONFIG: sandbox cannot be enabled in production");
|
||||
},
|
||||
migrations(context: PluginContext) {
|
||||
return context.metadata.get(key)
|
||||
? [
|
||||
{ id: "wrnexus-payment-001", source: migration },
|
||||
{ id: "wrnexus-payment-002-event-order", source: orderingMigration },
|
||||
]
|
||||
: [];
|
||||
},
|
||||
documentation: [join(root, "README.md")],
|
||||
});
|
||||
}
|
||||
export default paymentPlugin;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { getPaymentService } from "./core.ts";
|
||||
|
||||
export async function POST(ctx: { req: Request; params?: Record<string, string> }) {
|
||||
const gateway = ctx.params?.gateway?.trim();
|
||||
if (!gateway) return Response.json({ error: "payment gateway is required" }, { status: 400 });
|
||||
return getPaymentService().paymentWebhookHandler(gateway)(ctx);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import type {
|
||||
PaymentEvent,
|
||||
PaymentIntent,
|
||||
PaymentMethod,
|
||||
PaymentRefund,
|
||||
PaymentStore,
|
||||
} from "./types.ts";
|
||||
|
||||
export function memoryPaymentStore(): PaymentStore {
|
||||
const intents = new Map<string, PaymentIntent>();
|
||||
const keys = new Map<string, string>();
|
||||
const eventRows = new Map<string, PaymentEvent>();
|
||||
const refundRows = new Map<string, PaymentRefund>();
|
||||
const methodRows = new Map<string, PaymentMethod>();
|
||||
let lock = Promise.resolve();
|
||||
const exclusive = async <T>(run: () => T | Promise<T>): Promise<T> => {
|
||||
const previous = lock;
|
||||
let release!: () => void;
|
||||
lock = new Promise<void>((resolve) => (release = resolve));
|
||||
await previous;
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
return {
|
||||
async findIntentByKey(key) {
|
||||
const id = keys.get(key);
|
||||
return id ? structuredClone(intents.get(id)!) : null;
|
||||
},
|
||||
async putIntent(intent) {
|
||||
intents.set(intent.id, structuredClone(intent));
|
||||
keys.set(intent.idempotencyKey, intent.id);
|
||||
},
|
||||
async getIntent(id) {
|
||||
const row = intents.get(id);
|
||||
return row ? structuredClone(row) : null;
|
||||
},
|
||||
async listIntents(filter = {}) {
|
||||
return [...intents.values()]
|
||||
.filter(
|
||||
(row) =>
|
||||
(!filter.subject || row.subject === filter.subject) &&
|
||||
(!filter.gateway || row.gateway === filter.gateway),
|
||||
)
|
||||
.map((row) => structuredClone(row));
|
||||
},
|
||||
async appendEvent(event) {
|
||||
if (eventRows.has(event.id)) return false;
|
||||
eventRows.set(event.id, structuredClone(event));
|
||||
return true;
|
||||
},
|
||||
async events(intentId) {
|
||||
return [...eventRows.values()]
|
||||
.filter((row) => row.intentId === intentId)
|
||||
.sort((a, b) => a.at - b.at)
|
||||
.map((row) => structuredClone(row));
|
||||
},
|
||||
async reserveRefund(refund, captured) {
|
||||
return exclusive(() => {
|
||||
if ([...refundRows.values()].some((row) => row.idempotencyKey === refund.idempotencyKey))
|
||||
return false;
|
||||
const used = [...refundRows.values()]
|
||||
.filter((row) => row.intentId === refund.intentId && row.status !== "failed")
|
||||
.reduce((sum, row) => sum + row.amount, 0);
|
||||
if (used + refund.amount > captured) return false;
|
||||
refundRows.set(refund.id, structuredClone(refund));
|
||||
return true;
|
||||
});
|
||||
},
|
||||
async updateRefund(id, status, gatewayRef) {
|
||||
const row = refundRows.get(id);
|
||||
if (row) refundRows.set(id, { ...row, status, gatewayRef });
|
||||
},
|
||||
async refunds(intentId) {
|
||||
return [...refundRows.values()]
|
||||
.filter((row) => row.intentId === intentId)
|
||||
.map((row) => structuredClone(row));
|
||||
},
|
||||
async methods(subject) {
|
||||
return [...methodRows.values()]
|
||||
.filter((row) => row.subject === subject)
|
||||
.map((row) => structuredClone(row));
|
||||
},
|
||||
async getMethod(id) {
|
||||
const row = methodRows.get(id);
|
||||
return row ? structuredClone(row) : null;
|
||||
},
|
||||
async putMethod(method) {
|
||||
methodRows.set(method.id, structuredClone(method));
|
||||
},
|
||||
async removeMethod(id) {
|
||||
methodRows.delete(id);
|
||||
},
|
||||
async defaultMethod(subject, id) {
|
||||
for (const [key, row] of methodRows)
|
||||
if (row.subject === subject) methodRows.set(key, { ...row, default: key === id });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
export type PaymentStatus =
|
||||
| "created"
|
||||
| "pending"
|
||||
| "authorized"
|
||||
| "captured"
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "partially_refunded"
|
||||
| "refunded";
|
||||
|
||||
export interface Money {
|
||||
amount: number;
|
||||
currency: string;
|
||||
}
|
||||
export interface GatewayCapabilities {
|
||||
authorizeThenCapture: boolean;
|
||||
partialRefund: boolean;
|
||||
multipleRefunds: boolean;
|
||||
storedMethods: boolean;
|
||||
customerVault: boolean;
|
||||
hostedFields: boolean;
|
||||
hostedCheckout: boolean;
|
||||
webhookSignature: boolean;
|
||||
payouts: boolean;
|
||||
disputes: boolean;
|
||||
}
|
||||
export interface GatewaySupport {
|
||||
currencies: readonly string[] | "any";
|
||||
countries: readonly string[] | "any";
|
||||
}
|
||||
export interface CreateIntentInput extends Money {
|
||||
id: string;
|
||||
subject: string;
|
||||
idempotencyKey: string;
|
||||
country?: string;
|
||||
capture?: "automatic" | "manual";
|
||||
metadata?: Record<string, string>;
|
||||
returnUrl?: string;
|
||||
}
|
||||
export interface GatewayIntent {
|
||||
ref: string;
|
||||
status: PaymentStatus;
|
||||
amount: number;
|
||||
currency: string;
|
||||
checkoutUrl?: string;
|
||||
clientSecret?: string;
|
||||
raw?: unknown;
|
||||
}
|
||||
export interface GatewayRefund {
|
||||
ref: string;
|
||||
status: "pending" | "succeeded" | "failed";
|
||||
amount: number;
|
||||
raw?: unknown;
|
||||
}
|
||||
export interface GatewayMethod {
|
||||
ref: string;
|
||||
brand?: string;
|
||||
last4?: string;
|
||||
expires?: string;
|
||||
}
|
||||
export interface CustomerInput {
|
||||
subject: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
}
|
||||
export interface RefundInput extends Money {
|
||||
intentRef: string;
|
||||
idempotencyKey: string;
|
||||
reason: string;
|
||||
}
|
||||
export interface WebhookEvent {
|
||||
id: string;
|
||||
intentRef: string;
|
||||
type: string;
|
||||
status: PaymentStatus;
|
||||
/** Gateway-authored event time, never local arrival time. */
|
||||
occurredAt: number;
|
||||
/** Gateway-authored ordering token when the provider exposes one. */
|
||||
sequence?: string | number;
|
||||
payload: unknown;
|
||||
}
|
||||
|
||||
export interface PaymentGateway<TConfig = unknown> {
|
||||
readonly id: string;
|
||||
readonly config?: TConfig;
|
||||
readonly capabilities: GatewayCapabilities;
|
||||
readonly supports: GatewaySupport;
|
||||
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>;
|
||||
capture?(intentRef: string, amount?: Money): Promise<GatewayIntent>;
|
||||
listMethods?(customerRef: string): Promise<GatewayMethod[]>;
|
||||
attachMethod?(customerRef: string, token: string): Promise<GatewayMethod>;
|
||||
detachMethod?(methodRef: string): Promise<void>;
|
||||
createCustomer?(input: CustomerInput): Promise<string>;
|
||||
clientConfig(intent: GatewayIntent): Record<string, string>;
|
||||
}
|
||||
|
||||
export interface PaymentIntent extends Money {
|
||||
id: string;
|
||||
subject: string;
|
||||
gateway: string;
|
||||
gatewayRef: string;
|
||||
idempotencyKey: string;
|
||||
createdAt: number;
|
||||
checkoutUrl?: string;
|
||||
client: Record<string, string>;
|
||||
metadata: Record<string, string>;
|
||||
}
|
||||
export interface PaymentEvent {
|
||||
id: string;
|
||||
intentId: string;
|
||||
type: string;
|
||||
status: PaymentStatus;
|
||||
at: number;
|
||||
occurredAt?: number;
|
||||
sequence?: string | number;
|
||||
payload?: unknown;
|
||||
}
|
||||
export interface PaymentRefund {
|
||||
id: string;
|
||||
intentId: string;
|
||||
gatewayRef: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
reason: string;
|
||||
idempotencyKey: string;
|
||||
status: "pending" | "succeeded" | "failed";
|
||||
createdAt: number;
|
||||
}
|
||||
export interface PaymentMethod {
|
||||
id: string;
|
||||
subject: string;
|
||||
gateway: string;
|
||||
gatewayRef: string;
|
||||
brand?: string;
|
||||
last4?: string;
|
||||
expires?: string;
|
||||
default: boolean;
|
||||
}
|
||||
|
||||
export type PaymentResult<T> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; fault: "refused" | "gateway" | "storage" | "configuration"; reason: string };
|
||||
export interface PaymentStore {
|
||||
findIntentByKey(key: string): Promise<PaymentIntent | null>;
|
||||
putIntent(intent: PaymentIntent): Promise<void>;
|
||||
getIntent(id: string): Promise<PaymentIntent | null>;
|
||||
listIntents(filter?: { subject?: string; gateway?: string }): Promise<PaymentIntent[]>;
|
||||
appendEvent(event: PaymentEvent): Promise<boolean>;
|
||||
events(intentId: string): Promise<PaymentEvent[]>;
|
||||
reserveRefund(refund: PaymentRefund, captured: number): Promise<boolean>;
|
||||
updateRefund(id: string, status: PaymentRefund["status"], gatewayRef: string): Promise<void>;
|
||||
refunds(intentId: string): Promise<PaymentRefund[]>;
|
||||
methods(subject: string): Promise<PaymentMethod[]>;
|
||||
getMethod(id: string): Promise<PaymentMethod | null>;
|
||||
putMethod(method: PaymentMethod): Promise<void>;
|
||||
removeMethod(id: string): Promise<void>;
|
||||
defaultMethod(subject: string, id: string): Promise<void>;
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import {
|
||||
exerciseGatewayContract,
|
||||
paypalGateway,
|
||||
razorpayGateway,
|
||||
sandboxGateway,
|
||||
signSandboxWebhook,
|
||||
stripeGateway,
|
||||
type GatewayIntent,
|
||||
} from "../src/index.ts";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
const json = (value: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(value), { status, headers: { "content-type": "application/json" } });
|
||||
const encoder = new TextEncoder();
|
||||
async function signature(body: string, secret: string) {
|
||||
const key = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
encoder.encode(secret),
|
||||
{ name: "HMAC", hash: "SHA-256" },
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
return [...new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(body)))]
|
||||
.map((x) => x.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
const input = {
|
||||
id: "contract-pay",
|
||||
subject: "contract-user",
|
||||
amount: 1000,
|
||||
currency: "USD",
|
||||
idempotencyKey: "contract-init",
|
||||
};
|
||||
|
||||
test("shared executable contract drives sandbox", async () => {
|
||||
const adapter = sandboxGateway();
|
||||
const report = await exerciseGatewayContract(adapter, {
|
||||
input,
|
||||
async webhook(intent) {
|
||||
const signed = await signSandboxWebhook(
|
||||
{
|
||||
id: "sb-event",
|
||||
intentRef: intent.ref,
|
||||
type: "payment.captured",
|
||||
status: "captured",
|
||||
occurredAt: 100,
|
||||
},
|
||||
"contract-webhook-secret",
|
||||
);
|
||||
return new Request("https://test", {
|
||||
method: "POST",
|
||||
headers: { "x-wrnexus-signature": signed.signature },
|
||||
body: signed.body,
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(report).toEqual({ ok: true, issues: [] });
|
||||
});
|
||||
|
||||
test("shared executable contract drives Stripe through its HTTP adapter", async () => {
|
||||
globalThis.fetch = (async (value, init) => {
|
||||
const url = String(value),
|
||||
method = init?.method ?? "GET";
|
||||
if (
|
||||
url.includes("/payment_intents") &&
|
||||
method === "POST" &&
|
||||
!url.endsWith("/capture") &&
|
||||
!url.endsWith("/cancel")
|
||||
) {
|
||||
const body = String(init?.body);
|
||||
const id = body.includes("cancel") ? "pi_cancel" : "pi_contract";
|
||||
return json({
|
||||
id,
|
||||
status: "requires_capture",
|
||||
amount: 1000,
|
||||
amount_received: 1000,
|
||||
currency: "usd",
|
||||
client_secret: "pi_client_safe",
|
||||
});
|
||||
}
|
||||
if (url.endsWith("/capture"))
|
||||
return json({
|
||||
id: "pi_contract",
|
||||
status: "succeeded",
|
||||
amount_received: 1000,
|
||||
currency: "usd",
|
||||
});
|
||||
if (url.endsWith("/cancel"))
|
||||
return json({ id: "pi_cancel", status: "canceled", amount: 1000, currency: "usd" });
|
||||
if (url.includes("/payment_intents/"))
|
||||
return json({ id: "pi_contract", status: "requires_capture", amount: 1000, currency: "usd" });
|
||||
if (url.endsWith("/refunds")) return json({ id: "re_1", status: "succeeded", amount: 500 });
|
||||
if (url.endsWith("/customers")) return json({ id: "cus_1" });
|
||||
if (url.includes("/payment_methods?")) return json({ data: [] });
|
||||
if (url.includes("/payment_methods/") && url.endsWith("/attach"))
|
||||
return json({
|
||||
id: "pm_1",
|
||||
card: { brand: "visa", last4: "4242", exp_month: 1, exp_year: 2030 },
|
||||
});
|
||||
if (url.includes("/payment_methods/") && url.endsWith("/detach")) return json({ id: "pm_1" });
|
||||
return json({}, 404);
|
||||
}) as typeof fetch;
|
||||
const adapter = stripeGateway({
|
||||
secretKey: "sk_test_secret",
|
||||
publishableKey: "pk_test",
|
||||
webhookSecret: "contract-webhook-secret",
|
||||
});
|
||||
const report = await exerciseGatewayContract(adapter, {
|
||||
input,
|
||||
async webhook(intent) {
|
||||
const body = JSON.stringify({
|
||||
id: "evt_1",
|
||||
created: 100,
|
||||
data: { object: { id: intent.ref, status: "succeeded" } },
|
||||
type: "payment_intent.succeeded",
|
||||
});
|
||||
return new Request("https://test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"stripe-signature": `t=100,v1=${await signature(`100.${body}`, "contract-webhook-secret")}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(report).toEqual({ ok: true, issues: [] });
|
||||
});
|
||||
|
||||
test("shared executable contract drives Razorpay through its HTTP adapter", async () => {
|
||||
globalThis.fetch = (async (value, init) => {
|
||||
const url = String(value),
|
||||
method = init?.method ?? "GET";
|
||||
if (url.endsWith("/orders") && method === "POST") {
|
||||
const body = String(init?.body);
|
||||
return json({
|
||||
id: body.includes("cancel") ? "order_cancel" : "order_1",
|
||||
status: "created",
|
||||
amount: 1000,
|
||||
currency: "USD",
|
||||
});
|
||||
}
|
||||
if (url.includes("/orders/"))
|
||||
return json({
|
||||
id: url.endsWith("order_cancel") ? "order_cancel" : "order_1",
|
||||
status: "created",
|
||||
amount: 1000,
|
||||
currency: "USD",
|
||||
});
|
||||
if (url.includes("/refund")) return json({ id: "rfnd_1", status: "processed", amount: 500 });
|
||||
return json({}, 404);
|
||||
}) as typeof fetch;
|
||||
const adapter = razorpayGateway({
|
||||
keyId: "rzp_test",
|
||||
keySecret: "key_secret",
|
||||
webhookSecret: "contract-webhook-secret",
|
||||
});
|
||||
const report = await exerciseGatewayContract(adapter, {
|
||||
input,
|
||||
async webhook(intent) {
|
||||
const body = JSON.stringify({
|
||||
id: "rz_evt",
|
||||
event: "payment.captured",
|
||||
payload: {
|
||||
payment: {
|
||||
entity: { id: "pay_1", order_id: intent.ref, status: "captured", created_at: 100 },
|
||||
},
|
||||
},
|
||||
});
|
||||
return new Request("https://test", {
|
||||
method: "POST",
|
||||
headers: { "x-razorpay-signature": await signature(body, "contract-webhook-secret") },
|
||||
body,
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(report).toEqual({ ok: true, issues: [] });
|
||||
});
|
||||
|
||||
test("shared executable contract drives PayPal through its HTTP adapter", async () => {
|
||||
globalThis.fetch = (async (value, init) => {
|
||||
const url = String(value),
|
||||
method = init?.method ?? "GET";
|
||||
if (url.endsWith("/oauth2/token")) return json({ access_token: "access" });
|
||||
if (url.endsWith("/checkout/orders") && method === "POST") {
|
||||
const body = String(init?.body);
|
||||
return json({
|
||||
id: body.includes("cancel") ? "PP-CANCEL" : "PP-ORDER",
|
||||
status: "CREATED",
|
||||
links: [{ rel: "approve", href: "https://paypal.test/approve" }],
|
||||
});
|
||||
}
|
||||
if (url.includes("/checkout/orders/"))
|
||||
return json({
|
||||
id: url.endsWith("PP-CANCEL") ? "PP-CANCEL" : "PP-ORDER",
|
||||
status: "CREATED",
|
||||
purchase_units: [{ amount: { value: "10.00", currency_code: "USD" } }],
|
||||
});
|
||||
if (url.includes("/refund")) return json({ id: "PP-REFUND", status: "COMPLETED" });
|
||||
if (url.endsWith("verify-webhook-signature")) return json({ verification_status: "SUCCESS" });
|
||||
return json({}, 404);
|
||||
}) as typeof fetch;
|
||||
const adapter = paypalGateway({
|
||||
clientId: "client",
|
||||
clientSecret: "paypal_secret",
|
||||
webhookId: "hook",
|
||||
});
|
||||
const report = await exerciseGatewayContract(adapter, {
|
||||
input,
|
||||
webhook(intent: GatewayIntent) {
|
||||
return new Request("https://test", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"paypal-auth-algo": "SHA256withRSA",
|
||||
"paypal-cert-url": "https://paypal.test/cert",
|
||||
"paypal-transmission-id": "tx",
|
||||
"paypal-transmission-sig": "sig",
|
||||
"paypal-transmission-time": "time",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: "WH-1",
|
||||
create_time: "2026-08-23T00:00:00Z",
|
||||
event_type: "PAYMENT.CAPTURE.COMPLETED",
|
||||
resource: {
|
||||
id: "CAPTURE",
|
||||
status: "COMPLETED",
|
||||
supplementary_data: { related_ids: { order_id: intent.ref } },
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
expect(report).toEqual({ ok: true, issues: [] });
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { closeDatabases, setDb } from "@wrnexus/db";
|
||||
import { connectFromConfig } from "@wrnexus/db/connect";
|
||||
import { createPluginRunner } from "@wrnexus/plugin";
|
||||
import { parse } from "@wrnexus/syntax";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { databasePaymentStore, paymentPlugin } from "../src/index.ts";
|
||||
|
||||
afterEach(() => closeDatabases());
|
||||
test("payment plugin contributes its schema only when configured", async () => {
|
||||
const metadata = new Map<string, unknown>();
|
||||
const context = {
|
||||
root: process.cwd(),
|
||||
mode: "development" as const,
|
||||
command: "dev" as const,
|
||||
metadata,
|
||||
warn() {},
|
||||
};
|
||||
const runner = createPluginRunner(paymentPlugin(), context);
|
||||
await runner.configure({ payment: { default: "sandbox", sandbox: true } } as any);
|
||||
const contributions = await runner.contributions();
|
||||
expect(contributions.migrations.map((row) => row.id)).toEqual([
|
||||
"wrnexus-payment-001",
|
||||
"wrnexus-payment-002-event-order",
|
||||
]);
|
||||
expect(contributions.componentDirs).toHaveLength(1);
|
||||
expect(contributions.routes.map((row) => row.path)).toEqual(["/api/payments/webhook/:gateway"]);
|
||||
const off = createPluginRunner(paymentPlugin(), { ...context, metadata: new Map() });
|
||||
await off.configure({} as any);
|
||||
expect((await off.contributions()).migrations).toHaveLength(0);
|
||||
expect((await off.contributions()).routes).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("database store persists append-only events and atomically caps refunds", async () => {
|
||||
const db = connectFromConfig({ driver: "sqlite", url: ":memory:" });
|
||||
setDb(db);
|
||||
const metadata = new Map<string, unknown>();
|
||||
const runner = createPluginRunner(paymentPlugin(), {
|
||||
root: process.cwd(),
|
||||
mode: "development",
|
||||
command: "dev",
|
||||
metadata,
|
||||
warn() {},
|
||||
});
|
||||
await runner.configure({ payment: { default: "sandbox" } } as any);
|
||||
for (const migration of (await runner.contributions()).migrations) {
|
||||
const up = migration.source!.split("-- +down")[0].replace("-- +up", "");
|
||||
for (const statement of up
|
||||
.split(";")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean))
|
||||
await db.exec(statement);
|
||||
}
|
||||
const store = databasePaymentStore();
|
||||
const intent = {
|
||||
id: "pay-1",
|
||||
subject: "u",
|
||||
gateway: "sandbox",
|
||||
gatewayRef: "sb-1",
|
||||
amount: 100,
|
||||
currency: "USD",
|
||||
idempotencyKey: "once",
|
||||
createdAt: 1,
|
||||
client: {},
|
||||
metadata: {},
|
||||
};
|
||||
await store.putIntent(intent);
|
||||
expect((await store.findIntentByKey("once"))?.id).toBe("pay-1");
|
||||
expect(
|
||||
await store.appendEvent({
|
||||
id: "e1",
|
||||
intentId: "pay-1",
|
||||
type: "captured",
|
||||
status: "captured",
|
||||
at: 1,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await store.appendEvent({
|
||||
id: "e1",
|
||||
intentId: "pay-1",
|
||||
type: "captured",
|
||||
status: "captured",
|
||||
at: 1,
|
||||
}),
|
||||
).toBe(false);
|
||||
const base = {
|
||||
intentId: "pay-1",
|
||||
gatewayRef: "",
|
||||
currency: "USD",
|
||||
reason: "test",
|
||||
status: "pending" as const,
|
||||
createdAt: 1,
|
||||
};
|
||||
const results = await Promise.all([
|
||||
store.reserveRefund({ id: "r1", amount: 60, idempotencyKey: "r1", ...base }, 100),
|
||||
store.reserveRefund({ id: "r2", amount: 60, idempotencyKey: "r2", ...base }, 100),
|
||||
]);
|
||||
expect(results.filter(Boolean)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("all payment components parse and never collect raw card data", () => {
|
||||
for (const file of readdirSync(join(import.meta.dir, "../components")).filter((x) =>
|
||||
x.endsWith(".wrn"),
|
||||
)) {
|
||||
const source = readFileSync(join(import.meta.dir, "../components", file), "utf8");
|
||||
expect(() => parse(source)).not.toThrow();
|
||||
expect(source).not.toMatch(/cardNumber|cvv|\bpan\b/i);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
defineGateway,
|
||||
definePayment,
|
||||
memoryPaymentStore,
|
||||
paypalGateway,
|
||||
razorpayGateway,
|
||||
sandboxGateway,
|
||||
signSandboxWebhook,
|
||||
stripeGateway,
|
||||
derivePaymentStatus,
|
||||
verifyGatewayContract,
|
||||
} from "../src/index.ts";
|
||||
|
||||
test("gateway occurrence order and sticky terminal states prevent webhook regression", () => {
|
||||
const events = [
|
||||
{
|
||||
id: "refund",
|
||||
intentId: "p",
|
||||
type: "refunded",
|
||||
status: "refunded" as const,
|
||||
at: 100,
|
||||
occurredAt: 300,
|
||||
},
|
||||
{
|
||||
id: "late-capture-retry",
|
||||
intentId: "p",
|
||||
type: "captured",
|
||||
status: "captured" as const,
|
||||
at: 400,
|
||||
occurredAt: 200,
|
||||
},
|
||||
];
|
||||
expect(derivePaymentStatus(events)).toBe("refunded");
|
||||
expect(
|
||||
derivePaymentStatus([...events, { ...events[1], id: "new-id", at: 500, occurredAt: 500 }]),
|
||||
).toBe("refunded");
|
||||
});
|
||||
|
||||
test("sandbox drives webhook-authoritative capture and bounded partial refunds", async () => {
|
||||
const store = memoryPaymentStore(),
|
||||
gateway = sandboxGateway({ secret: "test-secret" });
|
||||
const payment = definePayment({
|
||||
store,
|
||||
adapters: [gateway],
|
||||
default: "sandbox",
|
||||
webhookSecrets: { sandbox: "test-secret" },
|
||||
environment: "test",
|
||||
});
|
||||
const created = await payment.initializePayment({
|
||||
subject: "user-1",
|
||||
amount: 1000,
|
||||
currency: "USD",
|
||||
idempotencyKey: "checkout-1",
|
||||
});
|
||||
expect(created.ok).toBe(true);
|
||||
if (!created.ok) return;
|
||||
expect(await payment.checkPayment(created.value.id)).toBe("pending");
|
||||
// A browser redirect cannot advance local state.
|
||||
expect(await payment.checkPayment(created.value.id)).toBe("pending");
|
||||
const signed = await signSandboxWebhook(
|
||||
{
|
||||
id: "evt-1",
|
||||
intentRef: created.value.gatewayRef,
|
||||
type: "payment.captured",
|
||||
status: "captured",
|
||||
},
|
||||
"test-secret",
|
||||
);
|
||||
const handler = payment.paymentWebhookHandler("sandbox");
|
||||
expect(
|
||||
(
|
||||
await handler({
|
||||
req: new Request("https://app.test/webhook", {
|
||||
method: "POST",
|
||||
headers: { "x-wrnexus-signature": signed.signature },
|
||||
body: signed.body,
|
||||
}),
|
||||
})
|
||||
).status,
|
||||
).toBe(200);
|
||||
expect(await payment.checkPayment(created.value.id)).toBe("captured");
|
||||
const first = await payment.refundPayment({
|
||||
id: created.value.id,
|
||||
amount: 400,
|
||||
reason: "partial return",
|
||||
idempotencyKey: "refund-1",
|
||||
});
|
||||
expect(first.ok).toBe(true);
|
||||
expect((await payment.refundableAmount(created.value.id)).amount).toBe(600);
|
||||
const excessive = await payment.refundPayment({
|
||||
id: created.value.id,
|
||||
amount: 601,
|
||||
reason: "too much",
|
||||
idempotencyKey: "refund-2",
|
||||
});
|
||||
expect(excessive).toMatchObject({ ok: false, fault: "refused" });
|
||||
expect(
|
||||
(
|
||||
await payment.refundPayment({
|
||||
id: created.value.id,
|
||||
amount: 600,
|
||||
reason: "remaining return",
|
||||
idempotencyKey: "refund-remaining",
|
||||
})
|
||||
).ok,
|
||||
).toBe(true);
|
||||
const stale = await signSandboxWebhook(
|
||||
{
|
||||
id: "evt-delayed-capture",
|
||||
intentRef: created.value.gatewayRef,
|
||||
type: "payment.captured",
|
||||
status: "captured",
|
||||
occurredAt: 1,
|
||||
},
|
||||
"test-secret",
|
||||
);
|
||||
await handler({
|
||||
req: new Request("https://app.test/webhook", {
|
||||
method: "POST",
|
||||
headers: { "x-wrnexus-signature": stale.signature },
|
||||
body: stale.body,
|
||||
}),
|
||||
});
|
||||
expect(await payment.checkPayment(created.value.id)).toBe("refunded");
|
||||
});
|
||||
|
||||
test("initialize, webhook, and refund replay change records once", async () => {
|
||||
const store = memoryPaymentStore(),
|
||||
gateway = sandboxGateway();
|
||||
const payment = definePayment({
|
||||
store,
|
||||
adapters: [gateway],
|
||||
default: "sandbox",
|
||||
webhookSecrets: { sandbox: "sandbox-secret" },
|
||||
});
|
||||
const input = { subject: "user-2", amount: 500, currency: "EUR", idempotencyKey: "same" };
|
||||
const one = await payment.initializePayment(input),
|
||||
two = await payment.initializePayment(input);
|
||||
expect(one.ok && two.ok && one.value.id).toBe(two.ok ? two.value.id : "");
|
||||
if (!one.ok) return;
|
||||
const signed = await signSandboxWebhook({
|
||||
id: "evt-replay",
|
||||
intentRef: one.value.gatewayRef,
|
||||
type: "payment.captured",
|
||||
status: "captured",
|
||||
});
|
||||
const request = () =>
|
||||
new Request("https://app.test/webhook", {
|
||||
method: "POST",
|
||||
headers: { "x-wrnexus-signature": signed.signature },
|
||||
body: signed.body,
|
||||
});
|
||||
const handler = payment.paymentWebhookHandler("sandbox");
|
||||
await handler({ req: request() });
|
||||
const replay = await handler({ req: request() });
|
||||
expect(await replay.json()).toEqual({ received: true, duplicate: true });
|
||||
const refund = await payment.refundPayment({
|
||||
id: one.value.id,
|
||||
amount: 100,
|
||||
reason: "return",
|
||||
idempotencyKey: "same-refund",
|
||||
});
|
||||
const duplicate = await payment.refundPayment({
|
||||
id: one.value.id,
|
||||
amount: 100,
|
||||
reason: "return",
|
||||
idempotencyKey: "same-refund",
|
||||
});
|
||||
expect(refund.ok && duplicate.ok && refund.value.id).toBe(duplicate.ok ? duplicate.value.id : "");
|
||||
expect(await payment.listRefunds(one.value.id)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("selection and capability differences are refused before money moves", async () => {
|
||||
const sandbox = sandboxGateway();
|
||||
const noPartial = defineGateway({
|
||||
...sandbox,
|
||||
id: "full-only",
|
||||
capabilities: { ...sandbox.capabilities, partialRefund: false },
|
||||
});
|
||||
const payment = definePayment({
|
||||
store: memoryPaymentStore(),
|
||||
adapters: [sandbox, noPartial],
|
||||
default: "sandbox",
|
||||
route: (input) => (input.currency === "INR" ? "full-only" : "sandbox"),
|
||||
});
|
||||
const manual = await payment.initializePayment({
|
||||
subject: "u",
|
||||
amount: 100,
|
||||
currency: "INR",
|
||||
country: "IN",
|
||||
capture: "manual",
|
||||
idempotencyKey: "route",
|
||||
});
|
||||
expect(manual.ok).toBe(true);
|
||||
expect(verifyGatewayContract(sandbox)).toEqual({ ok: true, issues: [] });
|
||||
const broken = defineGateway({
|
||||
...sandbox,
|
||||
id: "broken",
|
||||
capabilities: { ...sandbox.capabilities, authorizeThenCapture: false },
|
||||
});
|
||||
expect(verifyGatewayContract(broken).ok).toBe(false);
|
||||
});
|
||||
|
||||
test("invalid webhook signatures never update payment status", async () => {
|
||||
const gateway = sandboxGateway(),
|
||||
payment = definePayment({
|
||||
store: memoryPaymentStore(),
|
||||
adapters: [gateway],
|
||||
default: "sandbox",
|
||||
webhookSecrets: { sandbox: "right" },
|
||||
});
|
||||
const created = await payment.initializePayment({
|
||||
subject: "u",
|
||||
amount: 100,
|
||||
currency: "USD",
|
||||
idempotencyKey: "bad-hook",
|
||||
});
|
||||
if (!created.ok) return;
|
||||
const response = await payment.paymentWebhookHandler("sandbox")({
|
||||
req: new Request("https://app.test", {
|
||||
method: "POST",
|
||||
headers: { "x-wrnexus-signature": "wrong" },
|
||||
body: JSON.stringify({ id: "bad", intentRef: created.value.gatewayRef, status: "captured" }),
|
||||
}),
|
||||
});
|
||||
expect(response.status).toBe(400);
|
||||
expect(await payment.checkPayment(created.value.id)).toBe("pending");
|
||||
});
|
||||
|
||||
test("every Tier 1 adapter passes the shared capability contract", () => {
|
||||
const adapters = [
|
||||
sandboxGateway(),
|
||||
stripeGateway({ secretKey: "sk_test", publishableKey: "pk_test", webhookSecret: "whsec" }),
|
||||
razorpayGateway({ keyId: "rzp_test", keySecret: "secret", webhookSecret: "hook" }),
|
||||
paypalGateway({ clientId: "client", clientSecret: "secret", webhookId: "hook" }),
|
||||
];
|
||||
for (const adapter of adapters)
|
||||
expect(verifyGatewayContract(adapter)).toEqual({ ok: true, issues: [] });
|
||||
});
|
||||
@@ -37,15 +37,15 @@ function createQueue(options?: QueueOptions): Queue;
|
||||
|
||||
#### `QueueOptions`
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------------------------------------ | ---------- | -------------------------------------------------------- |
|
||||
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
||||
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
||||
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
||||
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
||||
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
|
||||
| `capacity` | `number` | unlimited | Maximum queued plus active jobs before adds reject. |
|
||||
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
||||
| Option | Type | Default | Description |
|
||||
| ------------- | ------------------------------------ | ---------- | --------------------------------------------------------------------------------------- |
|
||||
| `maxAttempts` | `number` | `3` | Default max attempts per job before it is dead-lettered. |
|
||||
| `backoffMs` | `number` | `1000` | Base retry backoff in ms; doubles per attempt. |
|
||||
| `pollMs` | `number` | `250` | Poll interval used once `start()` is called (ms). |
|
||||
| `onFailed` | `(job: Job, error: unknown) => void` | — | Called when a job exhausts its attempts. |
|
||||
| `concurrency` | `number` | unlimited | Maximum jobs claimed by one `drain()` call. |
|
||||
| `capacity` | `number` | unlimited | Optional maximum queued jobs; omitted queues never scan merely to enforce a hidden cap. |
|
||||
| `now` | `() => number` | `Date.now` | Clock injection for deterministic tests. |
|
||||
|
||||
### `Queue`
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/queue",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.14",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -46,7 +46,7 @@ function lazyStore(resolve: () => Promise<QueueStore>): QueueStore {
|
||||
return (await ready()).list(name);
|
||||
},
|
||||
async findByIdempotencyKey(name, key) {
|
||||
return (await ready()).findByIdempotencyKey?.(name, key) ?? null;
|
||||
return (await ready()).findByIdempotencyKey(name, key);
|
||||
},
|
||||
async size() {
|
||||
return (await ready()).size?.() ?? (await (await ready()).list()).length;
|
||||
|
||||
@@ -14,7 +14,8 @@ export interface QueueStore {
|
||||
remove(id: string): Promise<void>;
|
||||
due(now: number, limit: number): Promise<Job[]>;
|
||||
list(name?: string): Promise<Job[]>;
|
||||
findByIdempotencyKey?(name: string, key: string): Promise<Job | null>;
|
||||
/** Indexed lookup; implementations must not scan the queue. */
|
||||
findByIdempotencyKey(name: string, key: string): Promise<Job | null>;
|
||||
size?(): Promise<number>;
|
||||
claim?(id: string, worker: string, leaseUntil: number, now?: number): Promise<boolean>;
|
||||
release?(id: string, worker: string): Promise<void>;
|
||||
@@ -175,7 +176,8 @@ export function createDurableQueue(options: DurableQueueOptions = {}): DurableQu
|
||||
const workerId = options.workerId?.trim() || `worker-${crypto.randomUUID()}`;
|
||||
const defaultMaxAttempts = positiveInteger(options.maxAttempts ?? 3, "queue maxAttempts");
|
||||
const concurrency = positiveInteger(options.concurrency ?? 10, "queue concurrency");
|
||||
const capacity = positiveInteger(options.capacity ?? 10_000, "queue capacity");
|
||||
const capacity = options.capacity ?? Number.POSITIVE_INFINITY;
|
||||
if (capacity !== Number.POSITIVE_INFINITY) positiveInteger(capacity, "queue capacity");
|
||||
const leaseMs = positiveInteger(options.leaseMs ?? 30_000, "queue leaseMs");
|
||||
const pollMs = positiveInteger(options.pollMs ?? 250, "queue pollMs");
|
||||
let sequence = 0;
|
||||
@@ -280,12 +282,13 @@ export function createDurableQueue(options: DurableQueueOptions = {}): DurableQu
|
||||
}
|
||||
|
||||
if (add.idempotencyKey) {
|
||||
const existing = store.findByIdempotencyKey
|
||||
? await store.findByIdempotencyKey(name, add.idempotencyKey)
|
||||
: (await store.list(name)).find((job) => job.idempotencyKey === add.idempotencyKey);
|
||||
const existing = await store.findByIdempotencyKey(name, add.idempotencyKey);
|
||||
if (existing) return existing as Job<T>;
|
||||
}
|
||||
if ((store.size ? await store.size() : (await store.list()).length) >= capacity) {
|
||||
if (
|
||||
capacity !== Number.POSITIVE_INFINITY &&
|
||||
(store.size ? await store.size() : (await store.list()).length) >= capacity
|
||||
) {
|
||||
throw new Error(`WRN-QUEUE-CAPACITY: queue capacity of ${capacity} reached`);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,19 @@ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queu
|
||||
const jobs = `${prefix}:jobs`;
|
||||
const due = `${prefix}:due`;
|
||||
const key = (id: string) => `${prefix}:job:${id}`;
|
||||
const idempotencyKey = (name: string, value: string) =>
|
||||
`${prefix}:idempotency:${encodeURIComponent(name)}:${encodeURIComponent(value)}`;
|
||||
return {
|
||||
async put(job) {
|
||||
const previousValue = await client.get(key(job.id));
|
||||
const previous = previousValue ? (JSON.parse(previousValue) as Job) : null;
|
||||
if (previous?.idempotencyKey && previous.idempotencyKey !== job.idempotencyKey) {
|
||||
await client.del(idempotencyKey(previous.name, previous.idempotencyKey));
|
||||
}
|
||||
await client.set(key(job.id), JSON.stringify(job));
|
||||
if (job.idempotencyKey) {
|
||||
await client.set(idempotencyKey(job.name, job.idempotencyKey), job.id);
|
||||
}
|
||||
await client.sadd(jobs, job.id);
|
||||
await client.zadd(due, job.runAt, job.id);
|
||||
await client.del(`${prefix}:lease:${job.id}`);
|
||||
@@ -35,7 +45,11 @@ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queu
|
||||
return value ? (JSON.parse(value) as Job) : null;
|
||||
},
|
||||
async remove(id) {
|
||||
await client.del(key(id), `${prefix}:lease:${id}`);
|
||||
const value = await client.get(key(id));
|
||||
const job = value ? (JSON.parse(value) as Job) : null;
|
||||
const keys = [key(id), `${prefix}:lease:${id}`];
|
||||
if (job?.idempotencyKey) keys.push(idempotencyKey(job.name, job.idempotencyKey));
|
||||
await client.del(...keys);
|
||||
await client.srem(jobs, id);
|
||||
await client.zrem(due, id);
|
||||
},
|
||||
@@ -55,13 +69,10 @@ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queu
|
||||
.filter((job) => !name || job.name === name);
|
||||
},
|
||||
async findByIdempotencyKey(name, idempotencyKey) {
|
||||
const ids = await client.smembers(jobs);
|
||||
const values = await Promise.all(ids.map((id) => client.get(key(id))));
|
||||
const found = values
|
||||
.filter((value): value is string => value !== null)
|
||||
.map((value) => JSON.parse(value) as Job)
|
||||
.find((job) => job.name === name && job.idempotencyKey === idempotencyKey);
|
||||
return found ?? null;
|
||||
const id = await client.get(
|
||||
`${prefix}:idempotency:${encodeURIComponent(name)}:${encodeURIComponent(idempotencyKey)}`,
|
||||
);
|
||||
return id ? this.get(id) : null;
|
||||
},
|
||||
async size() {
|
||||
return (await client.smembers(jobs)).length;
|
||||
|
||||
@@ -260,6 +260,21 @@ test("durable queue supports capacity, cancellation, and shutdown", async () =>
|
||||
await expect(queue.add("work", {})).rejects.toThrow("WRN-QUEUE-CLOSED");
|
||||
});
|
||||
|
||||
test("durable queue has no implicit capacity scan or 10k ceiling", async () => {
|
||||
const base = memoryQueueStore();
|
||||
let sizeCalls = 0;
|
||||
const store = {
|
||||
...base,
|
||||
async size() {
|
||||
sizeCalls++;
|
||||
return 10_001;
|
||||
},
|
||||
};
|
||||
const queue = createDurableQueue({ store });
|
||||
await expect(queue.add("work", {})).resolves.toMatchObject({ name: "work" });
|
||||
expect(sizeCalls).toBe(0);
|
||||
});
|
||||
|
||||
test("force shutdown aborts an active durable job", async () => {
|
||||
const store = memoryQueueStore();
|
||||
const queue = createDurableQueue({ store });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/store",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.12",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { StoreContainer, defineStore } from "../src/index.ts";
|
||||
|
||||
test("memory persistence includes only approved fields and migrates old state", async () => {
|
||||
const original = defineStore({
|
||||
name: "preferences-migration",
|
||||
kind: "global" as const,
|
||||
createSharedState: () => ({ theme: "dark", secret: "initial" }),
|
||||
persist: { storage: "memory" as const, version: 1, include: ["theme"] },
|
||||
actions: {
|
||||
setTheme: {
|
||||
runtime: "client" as const,
|
||||
handler: ({ state }: any, theme: string) => void (state.theme = theme),
|
||||
},
|
||||
},
|
||||
});
|
||||
const first = await new StoreContainer({ runtime: "client" }).use(original);
|
||||
await first.actions.setTheme("light");
|
||||
|
||||
const migrated = defineStore({
|
||||
...original,
|
||||
persist: {
|
||||
storage: "memory" as const,
|
||||
version: 2,
|
||||
include: ["theme"],
|
||||
migrate: (state: any) => ({ ...state, theme: `${state.theme}-v2` }),
|
||||
},
|
||||
});
|
||||
const second = await new StoreContainer({ runtime: "client" }).use(migrated);
|
||||
expect(second.state.theme).toBe("light-v2");
|
||||
expect(second.state.secret).toBe("initial");
|
||||
});
|
||||
|
||||
test("invalid persisted state fails closed to the store defaults", async () => {
|
||||
const name = `preferences-validation-${crypto.randomUUID()}`;
|
||||
const firstDefinition = defineStore({
|
||||
name,
|
||||
kind: "global" as const,
|
||||
createSharedState: () => ({ theme: "dark" }),
|
||||
persist: { storage: "memory" as const, version: 1, include: ["theme"] },
|
||||
actions: {
|
||||
setTheme: {
|
||||
runtime: "client" as const,
|
||||
handler: ({ state }: any, theme: string) => void (state.theme = theme),
|
||||
},
|
||||
},
|
||||
});
|
||||
const first = await new StoreContainer({ runtime: "client" }).use(firstDefinition);
|
||||
await first.actions.setTheme("corrupt");
|
||||
|
||||
const validated = defineStore({
|
||||
...firstDefinition,
|
||||
persist: {
|
||||
storage: "memory" as const,
|
||||
version: 1,
|
||||
include: ["theme"],
|
||||
validate: (value: any) => (value?.theme === "light" ? value : null),
|
||||
},
|
||||
});
|
||||
const second = await new StoreContainer({ runtime: "client" }).use(validated);
|
||||
expect(second.state.theme).toBe("dark");
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/styles",
|
||||
"version": "0.8.19",
|
||||
"version": "0.8.20",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -313,6 +313,14 @@ export interface AppConfig {
|
||||
queue?: string | boolean;
|
||||
sandbox?: { enabled?: boolean; allowlist?: string[] };
|
||||
};
|
||||
/** Payment gateway configuration consumed by @wrnexus/payment. */
|
||||
payment?: {
|
||||
default: string;
|
||||
currency?: string;
|
||||
sandbox?: boolean;
|
||||
databaseName?: string;
|
||||
gateways?: Record<string, Record<string, unknown>>;
|
||||
};
|
||||
/**
|
||||
* File-upload storage. Declare named stores (local dir or S3-compatible),
|
||||
* upload with `handleUpload`/`upload` from `@wrnexus/uploader`, and serve
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/syntax",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.18",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -767,6 +767,28 @@ 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: string, options: FormatWrnOptions = {}): string {
|
||||
const unit = options.insertSpaces === false ? "\t" : " ".repeat(options.tabSize ?? 4);
|
||||
|
||||
@@ -844,6 +866,18 @@ function formatWrnPass(source: string, options: FormatWrnOptions = {}): string {
|
||||
continue;
|
||||
}
|
||||
|
||||
const structuredProp = formatStructuredPropDeclaration(
|
||||
value,
|
||||
unit,
|
||||
codeDepth + htmlDepth + controlDepth,
|
||||
printWidth,
|
||||
);
|
||||
if (structuredProp) {
|
||||
output.push(...structuredProp);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const leadingClosingBraces = countLeadingClosingBraces(value);
|
||||
|
||||
const closesControlBlock = isControlBlockClose(value);
|
||||
|
||||
@@ -2,6 +2,8 @@ export { Lexer, LexError, skipLiteralOrComment } from "./tokenizer.ts";
|
||||
export { formatWrn } from "./formatter.ts";
|
||||
export type { FormatWrnOptions } from "./formatter.ts";
|
||||
export { parse, parseHtmlView, ParseError, VOID_ELEMENTS } from "./parser.ts";
|
||||
export { extractInterpolations } from "./interpolation.ts";
|
||||
export type { InterpolationSegment } from "./interpolation.ts";
|
||||
export type {
|
||||
ActionBlock,
|
||||
ApiBlock,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
export interface InterpolationSegment {
|
||||
expression: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/** Extract top-level `{...}` expressions while preserving nested object/array literals. */
|
||||
export function extractInterpolations(value: string): InterpolationSegment[] {
|
||||
const segments: InterpolationSegment[] = [];
|
||||
let index = 0;
|
||||
while (index < value.length) {
|
||||
if (value[index] !== "{") {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
const start = index++;
|
||||
let depth = 1;
|
||||
let quote: string | null = 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;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { hasRequestSection, parseApiEntries, parseApiSections } from "../src/api-sections.ts";
|
||||
|
||||
test("api section scanning ignores keywords in literals, comments, and nested objects", () => {
|
||||
const source = `
|
||||
// request { body { forged: string } }
|
||||
response {
|
||||
return { label: "request { not a section }", nested: { body: true } }
|
||||
}
|
||||
`;
|
||||
expect(hasRequestSection(source)).toBe(false);
|
||||
expect(parseApiSections(source)?.response).toContain("nested");
|
||||
});
|
||||
|
||||
test("api entries normalize methods and reject malformed request fields", () => {
|
||||
expect(
|
||||
parseApiEntries(`lookup get /users/:id {
|
||||
response { return data }
|
||||
}`)[0],
|
||||
).toMatchObject({
|
||||
name: "lookup",
|
||||
method: "GET",
|
||||
path: "/users/:id",
|
||||
});
|
||||
expect(() =>
|
||||
parseApiSections("request { body { missingType } } response { return data }"),
|
||||
).toThrow('Expected "name: type"');
|
||||
});
|
||||
|
||||
test("api entries reject truncated blocks and bare response bodies", () => {
|
||||
expect(() => parseApiEntries("lookup GET /users { response { return data }")).toThrow();
|
||||
expect(() => parseApiEntries("lookup GET /users { return data }")).toThrow(
|
||||
'declare a "response { }" section',
|
||||
);
|
||||
});
|
||||
@@ -83,3 +83,18 @@ test("preserves balanced compact sibling markup without indentation drift", () =
|
||||
expect(formatted).toContain('<div class="strip"><span>Page</span><b>→</b><span>API</span></div>');
|
||||
expect(formatWrn(formatted, { tabSize: 2, printWidth: 80 })).toBe(formatted);
|
||||
});
|
||||
|
||||
test("formats large typed JSON prop defaults and remains idempotent", () => {
|
||||
const source = `component Catalog {
|
||||
props {
|
||||
categories: unknown[] = [{"label":"Platform & Core","value":"platform","apps":[{"label":"WRNexus Home / Business Command Center","href":"/products/home","number":"01"},{"label":"SSO & Account","href":"/products/sso","number":"02"}]},{"label":"Sales & Customer","value":"sales","apps":[{"label":"CRM","href":"/products/crm","number":"11"}]}]
|
||||
}
|
||||
view { <div></div> }
|
||||
}`;
|
||||
const formatted = formatWrn(source, options);
|
||||
|
||||
expect(formatted).toContain(`categories: unknown[] = [\n {\n "label": "Platform & Core"`);
|
||||
expect(formatted).toContain(`"apps": [\n {`);
|
||||
expect(formatWrn(formatted, options)).toBe(formatted);
|
||||
expect(() => parse(formatted)).not.toThrow();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { diagnose, formatDiagnostic, parse } from "../src/index.ts";
|
||||
import { diagnose, extractInterpolations, formatDiagnostic, parse } from "../src/index.ts";
|
||||
|
||||
test("parses explicit rendering modes and the never hydration alias", () => {
|
||||
const ast = parse(`page Marketing {
|
||||
@@ -337,3 +337,14 @@ test("a block comment inside props is still refused with its own message", () =>
|
||||
`;
|
||||
expect(() => parse(source)).toThrow("Block comments are not allowed inside props");
|
||||
});
|
||||
|
||||
test("extracts nested object and array interpolation as one expression", () => {
|
||||
const value = `prefix {[{ label: 'One', meta: { count: 1 } }, { label: "Two" }]} suffix`;
|
||||
expect(extractInterpolations(value)).toEqual([
|
||||
{
|
||||
expression: `[{ label: 'One', meta: { count: 1 } }, { label: "Two" }]`,
|
||||
start: 7,
|
||||
end: value.length - 7,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/test",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -205,3 +205,5 @@ export type {
|
||||
} from "./advanced.ts";
|
||||
export { withDatabaseRollback, createFactory, captureBrowserArtifacts } from "./platform.ts";
|
||||
export type { TransactionalDatabase, BrowserArtifactPage } from "./platform.ts";
|
||||
export { detectMutations } from "./mutation.ts";
|
||||
export type { MutationCase, MutationReport, DetectMutationsOptions } from "./mutation.ts";
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface MutationCase<T> {
|
||||
name: string;
|
||||
value: T;
|
||||
}
|
||||
|
||||
export interface MutationReport<O> {
|
||||
baseline: O;
|
||||
killed: string[];
|
||||
survived: string[];
|
||||
}
|
||||
|
||||
export interface DetectMutationsOptions<T, O> {
|
||||
baseline: T;
|
||||
mutations: Array<MutationCase<T>>;
|
||||
exercise(value: T): O | Promise<O>;
|
||||
equivalent?(left: O, right: O): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the same behavioural probe against a baseline and explicit mutations.
|
||||
* A mutation survives when the observable result is unchanged. The helper
|
||||
* throws with every survivor by default, making it suitable for an ordinary
|
||||
* Bun test and CI without a second runner or source-rewriting process.
|
||||
*/
|
||||
export async function detectMutations<T, O>(
|
||||
options: DetectMutationsOptions<T, O>,
|
||||
): Promise<MutationReport<O>> {
|
||||
if (!options.mutations.length) {
|
||||
throw new Error("WRN-MUTATION-EMPTY: declare at least one meaningful mutation");
|
||||
}
|
||||
const equivalent = options.equivalent ?? ((left, right) => Object.is(left, right));
|
||||
const baseline = await options.exercise(options.baseline);
|
||||
const killed: string[] = [];
|
||||
const survived: string[] = [];
|
||||
for (const mutation of options.mutations) {
|
||||
if (!mutation.name.trim()) throw new Error("WRN-MUTATION-NAME: every mutation needs a name");
|
||||
let same = false;
|
||||
try {
|
||||
same = equivalent(baseline, await options.exercise(mutation.value));
|
||||
} catch {
|
||||
// A behavioural failure is a killed mutation.
|
||||
}
|
||||
(same ? survived : killed).push(mutation.name);
|
||||
}
|
||||
if (survived.length) {
|
||||
throw Object.assign(new Error(`WRN-MUTATION-SURVIVED: ${survived.join(", ")}`), {
|
||||
report: { baseline, killed, survived },
|
||||
});
|
||||
}
|
||||
return { baseline, killed, survived };
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { detectMutations } from "../src/mutation.ts";
|
||||
|
||||
test("detectMutations reports mutations whose behaviour changes", async () => {
|
||||
const report = await detectMutations({
|
||||
baseline: (amount: number) => amount,
|
||||
mutations: [
|
||||
{ name: "invert sign", value: (amount: number) => -amount },
|
||||
{ name: "drop amount", value: () => 0 },
|
||||
],
|
||||
exercise: (adjust) => adjust(7),
|
||||
});
|
||||
expect(report.killed).toEqual(["invert sign", "drop amount"]);
|
||||
expect(report.survived).toEqual([]);
|
||||
});
|
||||
|
||||
test("detectMutations fails with the names of surviving mutations", async () => {
|
||||
await expect(
|
||||
detectMutations({
|
||||
baseline: (amount: number) => amount,
|
||||
mutations: [{ name: "wrong implementation", value: (amount: number) => amount }],
|
||||
exercise: (adjust) => adjust(7),
|
||||
}),
|
||||
).rejects.toThrow(/WRN-MUTATION-SURVIVED: wrong implementation/);
|
||||
});
|
||||
|
||||
test("detectMutations accepts structured observations through an equivalence function", async () => {
|
||||
const report = await detectMutations({
|
||||
baseline: { status: 200 },
|
||||
mutations: [{ name: "deny request", value: { status: 403 } }],
|
||||
exercise: async (response) => ({ ...response, volatile: crypto.randomUUID() }),
|
||||
equivalent: (left, right) => left.status === right.status,
|
||||
});
|
||||
expect(report.killed).toEqual(["deny request"]);
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/typecheck",
|
||||
"version": "0.8.13",
|
||||
"version": "0.8.16",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { dirname, join, normalize, resolve } from "node:path";
|
||||
import ts from "typescript";
|
||||
import {
|
||||
containsReadonlyPropMutation,
|
||||
extractInterpolations,
|
||||
inferredRuntimeType,
|
||||
parse,
|
||||
runtimeTypeOf,
|
||||
@@ -139,7 +140,7 @@ function functionDeclaration(fn: RuntimeFunctionDecl): string {
|
||||
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}` : ""}`,
|
||||
`${param.name}${param.optional ? "?" : ""}${param.valueType ? `: ${param.valueType}` : param.name === "event" ? ": Event & { target: HTMLElement }" : ": any"}${param.default ? ` = ${param.default}` : ""}`,
|
||||
)
|
||||
.join(", ");
|
||||
// An omitted WRN parameter type follows JavaScript semantics. Emitting it as
|
||||
@@ -214,6 +215,31 @@ function safeBindingName(name: string): boolean {
|
||||
return /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED_BINDING_NAMES.has(name);
|
||||
}
|
||||
|
||||
function safeViewExpression(expression: string, reservedProps: ReadonlySet<string>): string {
|
||||
if (!reservedProps.size) return expression;
|
||||
const scanner = ts.createScanner(
|
||||
ts.ScriptTarget.Latest,
|
||||
false,
|
||||
ts.LanguageVariant.Standard,
|
||||
expression,
|
||||
);
|
||||
let result = "";
|
||||
let copied = 0;
|
||||
for (let token = scanner.scan(); token !== ts.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);
|
||||
// Property access and object-literal keys are already valid TypeScript.
|
||||
if (previous === "." || following === ":") continue;
|
||||
result += expression.slice(copied, start) + `props.${text}`;
|
||||
copied = end;
|
||||
}
|
||||
return result + expression.slice(copied);
|
||||
}
|
||||
|
||||
export function virtualTypeScriptModule(
|
||||
source: string,
|
||||
filePath = "component.wrn",
|
||||
@@ -307,6 +333,9 @@ export function virtualTypeScriptModule(
|
||||
if (ast.dataApis.length) append(`declare const api: { ${apiType} };`);
|
||||
append(`declare const props: Readonly<${ast.name}Props>;`);
|
||||
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>;",
|
||||
);
|
||||
@@ -348,21 +377,29 @@ export function virtualTypeScriptModule(
|
||||
// misspelled state/function name cannot silently become `undefined` at
|
||||
// runtime. Each/handler locals mirror the browser evaluator's bindings.
|
||||
let bindingIndex = 0;
|
||||
const reservedProps = new Set(
|
||||
ast.props.map((prop) => prop.name).filter((name) => !safeBindingName(name)),
|
||||
);
|
||||
const appendViewBindings = (nodes: ViewNode[], locals: Set<string>): void => {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "element") {
|
||||
for (const attr of node.attrs) {
|
||||
const expressions = attr.event
|
||||
? [attr.value]
|
||||
: [...attr.value.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]!.trim());
|
||||
: 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;"] : []),
|
||||
...(attr.event
|
||||
? [
|
||||
"declare const payload: any;",
|
||||
"declare const event: Event & { target: HTMLElement };",
|
||||
]
|
||||
: []),
|
||||
].join(" ");
|
||||
append(
|
||||
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${expression}); }`,
|
||||
`namespace __wrn_view_${bindingIndex++} { ${declarations} void (${safeViewExpression(expression, reservedProps)}); }`,
|
||||
attr.value,
|
||||
);
|
||||
}
|
||||
@@ -372,13 +409,16 @@ export function virtualTypeScriptModule(
|
||||
for (const branch of node.branches) {
|
||||
if (branch.cond)
|
||||
append(
|
||||
`namespace __wrn_view_${bindingIndex++} { void (${branch.cond}); }`,
|
||||
`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 (${node.list}); }`, node.list);
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { checkWrnSource } from "../src/index.ts";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { checkWrnSource } from "../src/index.ts";
|
||||
@@ -82,6 +82,23 @@ page Home {
|
||||
expect(diagnostics.some((item) => item.code === "WRN-COMPONENT-PROP-TYPE")).toBe(false);
|
||||
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1389")).toBe(false);
|
||||
expect(diagnostics.some((item) => item.code === "WRN-TYPE-1005")).toBe(false);
|
||||
expect(
|
||||
checkWrnSource(readFileSync(componentPath, "utf8"), { appRoot: root, filePath: componentPath }),
|
||||
).toEqual([]);
|
||||
}, 15_000);
|
||||
|
||||
test("implicit DOM events satisfy untyped event handler parameters", () => {
|
||||
const root = fixture();
|
||||
const filePath = join(root, "app", "components", "Submit.wrn");
|
||||
const diagnostics = checkWrnSource(
|
||||
`component Submit {
|
||||
outputs { submit(payload: { event: Event }) }
|
||||
functions { client function submitForm(event) { output.submit({ event }) } }
|
||||
view { <form @submit="submitForm(event)"></form> }
|
||||
}`,
|
||||
{ appRoot: root, filePath },
|
||||
);
|
||||
expect(diagnostics).toEqual([]);
|
||||
}, 15_000);
|
||||
|
||||
test("does not treat UI component imports as JavaScript exports", () => {
|
||||
|
||||
@@ -112,3 +112,32 @@ test("accepts handler payload, loop locals, and the public useFetch helper", ()
|
||||
);
|
||||
expect(diagnostics.filter((diagnostic) => diagnostic.code === "WRN-TYPE-2304")).toEqual([]);
|
||||
});
|
||||
|
||||
test("accepts multiline array props containing nested object literals", () => {
|
||||
const root = app();
|
||||
const diagnostics = checkWrnSource(
|
||||
`page Contact {
|
||||
view {
|
||||
<div data-component="StatsBar" items="{[
|
||||
{ label: 'Product catalog', value: '81', suffix: ' apps' },
|
||||
{ label: 'Starting point', value: 'One workflow' }
|
||||
]}"></div>
|
||||
}
|
||||
}`,
|
||||
{ appRoot: root, filePath: join(root, "app", "pages", "contact.wrn") },
|
||||
);
|
||||
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("provides the toast API to view event expressions", () => {
|
||||
const root = app();
|
||||
const diagnostics = checkWrnSource(
|
||||
`page Contact {
|
||||
view {
|
||||
<form @wrn:success="toast.success('Sent', { title: 'Request sent' })"></form>
|
||||
}
|
||||
}`,
|
||||
{ appRoot: root, filePath: join(root, "app", "pages", "contact.wrn") },
|
||||
);
|
||||
expect(diagnostics.filter((diagnostic) => diagnostic.code.startsWith("WRN-TYPE-"))).toEqual([]);
|
||||
});
|
||||
|
||||
+15
-15
@@ -45,7 +45,7 @@ Secure multi-cell PIN and verification-code input with regex and paste support.
|
||||
Theme-aware, responsive strong password component.
|
||||
|
||||
- Mount: `data-component="StrongPassword"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `class: string = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Password"`, `name: string = "password"`, `value: string = ""`, `placeholder: string = "Create a strong password"`, `autocomplete: string = "new-password"`, `minLength: number = 8`, `specialCharactersSet: string = "!@#$%^&*()_+-=[]{}|;:,.<>?"`, `requireLowercase: boolean = true`, `requireUppercase: boolean = true`, `requireNumber: boolean = true`, `requireSpecialCharacter: boolean = true`, `showRequirements: boolean = true`, `presentation: string = "inline"`, `hintText: string = "Use a unique password you do not use elsewhere."`, `emptyLabel: string = "Enter a password"`, `weakLabel: string = "Weak"`, `fairLabel: string = "Fair"`, `goodLabel: string = "Good"`, `strongLabel: string = "Strong"`, `disabled: boolean = false`, `readonly: boolean = false`, `required: boolean = false`, `invalid: boolean = false`, `validationMessage: string = ""`, `showLabel: string = "Show password"`, `hideLabel: string = "Hide password"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Outputs: `input({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `change({ value: string; score: number; maximumScore: number; percent: number; level: string })`, `strength({ value: string; score: number; maximumScore: number; percent: number; level: string })`
|
||||
|
||||
@@ -146,7 +146,7 @@ Theme-aware, responsive button group component.
|
||||
Group related content in a responsive themed surface with title, description, content, and supporting slots.
|
||||
|
||||
- Mount: `data-component="Card"`
|
||||
- Props: `title: string = "Card title"`, `subtitle: string = ""`, `description: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
|
||||
- Props: `title: string = ""`, `icon: string = ""`, `titleKey: string = ""`, `subtitle: string = ""`, `subtitleKey: string = ""`, `description: string = ""`, `descriptionKey: string = ""`, `header: string = ""`, `footer: string = ""`, `imageSrc: string = ""`, `imageAlt: string = ""`, `imagePosition: string = "top"`, `actionLabel: string = ""`, `actionLabelKey: string = ""`, `actionHref: string = ""`, `headerActions: unknown[] = []`, `navigation: unknown[] = []`, `activeNav: string = ""`, `mobileNavigation: boolean = false`, `alertTitle: string = ""`, `alertDescription: string = ""`, `empty: boolean = false`, `emptyTitle: string = "No data to show"`, `emptyIcon: string = "icon-[lucide--inbox]"`, `items: unknown[] = []`, `size: string = "md"`, `color: string = "primary"`, `variant: string = "default"`, `layout: string = "vertical"`, `align: string = "left"`, `hover: string = "none"`, `radius: string = "default"`, `shadow: string = "default"`, `equalHeight: boolean = false`, `scrollable: boolean = false`, `maxHeight: string = "18rem"`, `dismissible: boolean = false`, `ariaLabel: string = ""`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: `click({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `navigate({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `load({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `error({ error: Error | string; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | Error | string)`
|
||||
|
||||
@@ -337,7 +337,7 @@ Reusable portal dashboard component.
|
||||
Reusable preference switcher component.
|
||||
|
||||
- Mount: `data-component="PreferenceSwitcher"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `class: string = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `themeLabel: string = "Theme"`, `colorLabel: string = "Accent color"`, `languageLabel: string = "Language"`, `languages: string = [`, `colors: string = [`, `compact: boolean = true`, `iconOnly: boolean = true`, `themeIcon: string = "icon-[lucide--sun-moon]"`, `colorIcon: string = "icon-[lucide--palette]"`, `languageIcon: string = "icon-[lucide--languages]"`, `panelPlacement: string = "bottom-end"`, `mobilePanel: string = "dropdown"`, `showCurrentLanguage: boolean = true`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Outputs: `theme({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `color({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `language({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`
|
||||
|
||||
@@ -375,7 +375,7 @@ Arrange operational metrics, KPIs, public statistics, or service indicators in a
|
||||
Present a compact responsive strip of key facts, counts, performance indicators, or trust signals.
|
||||
|
||||
- Mount: `data-component="StatsBar"`
|
||||
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `class: string = ""`
|
||||
- Props: `items: unknown[] = []`, `columns: number = 4`, `compact: boolean = true`, `dividers: boolean = true`, `icons: boolean = true`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "raised"`, `maxWidth: string = "xl"`, `radius: string = "default"`, `shadow: string = "default"`, `gutter: string = "md"`, `outerSpacing: string = "none"`, `mobileColumns: number = 1`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Outputs: None
|
||||
|
||||
@@ -606,7 +606,7 @@ Arrange feature cards, services, solutions, or benefits in a responsive equal-he
|
||||
Render structured responsive footer navigation, pre and post content, copyright content, links, and public events.
|
||||
|
||||
- Mount: `data-component="Footer"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `copyright: string = ""`, `class: string = ""`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Footer navigation"`, `items: unknown[] = []`, `columns: number = 3`, `maxWidth: string = "compact"`, `mobileColumns: number = 2`, `mobileCopyrightAlign: string = "center"`, `copyright: string = ""`, `copyrightKey: string = ""`, `translationPrefix: string = ""`, `class: string = ""`
|
||||
- Slots: `pre-footer`, `post-footer`, `copyright-left`, `copyright-right`
|
||||
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`, `action({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||
|
||||
@@ -698,7 +698,7 @@ Apply consistent readable responsive typography, widths, columns, spacing, and e
|
||||
Publish a responsive notice with badge, icon, supporting copy, action, dismiss behavior, and width controls.
|
||||
|
||||
- Mount: `data-component="AnnouncementBar"`
|
||||
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
|
||||
- Props: `badge: string = ""`, `badgeIcon: string = ""`, `message: string = "Announcement"`, `description: string = ""`, `icon: string = "icon-[lucide--megaphone]"`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = ""`, `dismissible: boolean = false`, `dismissLabel: string = "Dismiss announcement"`, `sticky: boolean = false`, `compact: boolean = false`, `flush: boolean = false`, `mobileCompact: boolean = false`, `mobileMessageLines: number = 2`, `hideBadgeOnMobile: boolean = false`, `hideDescriptionOnMobile: boolean = false`, `mobileActionSize: string = "default"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `size: string = "default"`, `width: string = "default"`, `color: string = "primary"`, `variant: string = "soft"`, `role: string = "status"`, `live: string = "polite"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Outputs: `dismiss({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`
|
||||
|
||||
@@ -707,7 +707,7 @@ Publish a responsive notice with badge, icon, supporting copy, action, dismiss b
|
||||
Close a page or major section with conversion-focused copy, actions, and optional supporting visual content.
|
||||
|
||||
- Mount: `data-component="CTASection"`
|
||||
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
|
||||
- Props: `eyebrow: string = ""`, `title: string = "Ready to get started?"`, `description: string = ""`, `icon: string = ""`, `align: string = "center"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "solid"`, `spacing: string = "none"`, `radius: string = "default"`, `titleMaxWidth: string = ""`, `contentPadding: string = "default"`, `mobilePadding: string = "default"`, `mobileActions: string = "stack"`, `primaryLabel: string = "Get started"`, `primaryHref: string = "#"`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualPosition: string = "right"`, `maxWidth: string = "xl"`, `fullBleed: boolean = false`, `class: string = ""`
|
||||
- Slots: `default`, `actions`, `visual`, `footer`
|
||||
- Outputs: None
|
||||
|
||||
@@ -725,7 +725,7 @@ Present one linked feature or service with media, icon, badge, description, and
|
||||
Present a compact feature or benefit with a styled icon, title, description, badge, and optional link.
|
||||
|
||||
- Mount: `data-component="FeatureIconCard"`
|
||||
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `class: string = ""`
|
||||
- Props: `icon: string = "icon-[lucide--sparkles]"`, `iconSize: string = "md"`, `iconVariant: string = "soft"`, `title: string = "Feature"`, `description: string = ""`, `href: string = ""`, `actionLabel: string = "Explore"`, `badge: string = ""`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `align: string = "left"`, `hover: string = "lift"`, `radius: string = "default"`, `shadow: string = "default"`, `clickableSurface: boolean = false`, `descriptionTone: string = "default"`, `class: string = ""`
|
||||
- Slots: `default`, `footer`
|
||||
- Outputs: None
|
||||
|
||||
@@ -770,7 +770,7 @@ Introduce an internal or public page with breadcrumbs, icon, title, description,
|
||||
Build a responsive two-column introduction balancing descriptive content with a visual, image, or structured data panel.
|
||||
|
||||
- Mount: `data-component="SplitHero"`
|
||||
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
|
||||
- Props: `eyebrow: string = ""`, `eyebrowIcon: string = "icon-[lucide--sparkles]"`, `title: string = "A better digital experience"`, `highlight: string = ""`, `description: string = ""`, `primaryLabel: string = ""`, `primaryHref: string = ""`, `primaryIcon: string = ""`, `secondaryLabel: string = ""`, `secondaryHref: string = ""`, `secondaryIcon: string = ""`, `visualPosition: string = "right"`, `reverse: boolean = false`, `ratio: string = "balanced"`, `align: string = "left"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "default"`, `maxWidth: string = "xl"`, `fullBleed: boolean = true`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `titleSize: string = "default"`, `titleMaxWidth: string = ""`, `descriptionWeight: string = "default"`, `mobileTitleSize: string = "default"`, `mobileStack: boolean = true`, `backgroundImage: string = ""`, `visualImage: string = ""`, `visualAlt: string = ""`, `visualIcon: string = ""`, `visualEyebrow: string = ""`, `visualTitle: string = ""`, `visualDescription: string = ""`, `visualItems: unknown[] = []`, `visualStyle: string = "panel"`, `trustItems: unknown[] = []`, `class: string = ""`
|
||||
- Slots: `default`, `actions`, `trust`, `visual`
|
||||
- Outputs: None
|
||||
|
||||
@@ -799,7 +799,7 @@ Provide a responsive floating control that returns long pages to the top and can
|
||||
Show responsive hierarchical navigation with home support, separators, current-page state, sizes, and selection events.
|
||||
|
||||
- Mount: `data-component="Breadcrumb"`
|
||||
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
|
||||
- Props: `label: string = "Breadcrumb"`, `items: unknown[] = []`, `active: string = ""`, `activeIcon: string = "icon-[lucide--file-text]"`, `separator: string = "chevron"`, `showHome: boolean = false`, `homeLabel: string = "Home"`, `homeHref: string = "/"`, `homeIcon: string = "icon-[lucide--house]"`, `size: string = "default"`, `color: string = "primary"`, `variant: string = "minimal"`, `class: string = ""`
|
||||
- Slots: None
|
||||
- Outputs: `select({ value?: string | number | boolean | null; values?: Array<string | number | boolean | null | object>; sourceEvent?: Event; [key: string]: string | number | boolean | null | object } | string | number | boolean | null)`
|
||||
|
||||
@@ -808,9 +808,9 @@ Show responsive hierarchical navigation with home support, separators, current-p
|
||||
Theme-aware, responsive mega menu component.
|
||||
|
||||
- Mount: `data-component="MegaMenu"`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `label: string = "Menu"`, `icon: string = ""`, `columns: unknown[] = []`, `rail: unknown[] = []`, `featured: Record<string, unknown> = {}`, `variant: string = "default"`, `density: string = "default"`, `panelWidth: string = "xl"`, `panelAlign: string = "start"`, `columnCount: number = 0`, `fullWidth: boolean = false`, `openOnHover: boolean = true`, `activeCategory: string = ""`, `actionLabel: string = ""`, `actionHref: string = ""`, `actionIcon: string = "icon-[lucide--arrow-right]"`, `footer: string = ""`, `defaultOpen: boolean = false`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`
|
||||
- Outputs: `open({ sourceEvent: Event })`, `close({ reason: string })`, `select({ item: object; value: string })`, `category({ item: object; value: string; index: number })`
|
||||
|
||||
### Nav
|
||||
|
||||
@@ -826,8 +826,8 @@ Theme-aware, responsive nav component.
|
||||
Theme-aware, responsive navbar component.
|
||||
|
||||
- Mount: `data-component="Navbar"`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `class: string = ""`
|
||||
- Slots: `topbar`, `actions`
|
||||
- Props: `size: string = "default"`, `color: string = "primary"`, `label: string = "Primary navigation"`, `topbarLabel: string = "Utility navigation"`, `brand: Record<string, unknown> = {}`, `items: unknown[] = []`, `actions: unknown[] = []`, `active: string = ""`, `sticky: boolean = false`, `floating: boolean = false`, `startFloating: string = "0px"`, `openOnHover: boolean = false`, `maxWidth: string = "full"`, `mobileLabel: string = "Toggle navigation"`, `collapseAt: string = "900px"`, `mobilePanel: string = "content"`, `mobileUtilitiesPosition: string = "header"`, `gutter: string = "md"`, `contentGutter: string = "md"`, `flushMobile: boolean = false`, `translationPrefix: string = ""`, `class: string = ""`
|
||||
- Slots: `topbar`, `actions`, `utilities`
|
||||
- Outputs: `toggle({ open: boolean })`, `open({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `close({ sourceEvent?: Event; [key: string]: string | number | boolean | null | object })`, `select({ item: string | number | boolean | null | object; value: string | number | boolean; level: string })`, `action({ item: string | number | boolean | null | object; value: string | number | boolean })`
|
||||
|
||||
### Pagination
|
||||
@@ -844,7 +844,7 @@ Theme-aware, responsive pagination component.
|
||||
Theme-aware, responsive scrollspy component.
|
||||
|
||||
- Mount: `data-component="Scrollspy"`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `class: string = ""`
|
||||
- Props: `color: string = "primary"`, `size: string = "default"`, `items: unknown[] = []`, `active: string = ""`, `label: string = "On this page"`, `heading: string = ""`, `sticky: boolean = false`, `topOffset: string = "6rem"`, `sectionOffset: string = "7rem"`, `collapseAt: string = "800px"`, `class: string = ""`
|
||||
- Slots: `default`
|
||||
- Outputs: `change({ href: string; label: string })`
|
||||
|
||||
|
||||
@@ -90,6 +90,62 @@ the component directory at runtime.
|
||||
For the full catalog, see [`COMPONENTS.md`](./COMPONENTS.md). The machine-readable
|
||||
equivalent is exported as `@wrnexus/ui/component-reference.json`.
|
||||
|
||||
### Typed Navbar items
|
||||
|
||||
Keep navigation data in a shared TypeScript module and validate it with the
|
||||
package contract. `NavbarItem` is a discriminated union of `link`, `dropdown`,
|
||||
and `mega`; a mega item accepts every `MegaMenu` layout option.
|
||||
|
||||
```ts
|
||||
import type { NavbarItem } from "@wrnexus/ui";
|
||||
|
||||
export const navigation = [
|
||||
{ type: "link", label: "Pricing", href: "/pricing" },
|
||||
{
|
||||
type: "dropdown",
|
||||
label: "Company",
|
||||
items: [
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "mega",
|
||||
label: "Products",
|
||||
mega: {
|
||||
variant: "icon-grid",
|
||||
panelWidth: "2xl",
|
||||
columnCount: 3,
|
||||
rail: [{ label: "All products", href: "/products" }],
|
||||
columns: [
|
||||
{
|
||||
heading: "Work",
|
||||
items: [
|
||||
{
|
||||
label: "Projects",
|
||||
href: "/products/projects",
|
||||
icon: "icon-[lucide--folders]",
|
||||
description: "Plan and deliver work",
|
||||
badge: "New",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
featured: {
|
||||
title: "What's new",
|
||||
actionLabel: "Explore",
|
||||
actionHref: "/new",
|
||||
},
|
||||
actionLabel: "View all products",
|
||||
actionHref: "/products",
|
||||
},
|
||||
},
|
||||
] satisfies NavbarItem[];
|
||||
```
|
||||
|
||||
Importing from `@wrnexus/ui/navigation` is also supported. Existing untyped
|
||||
Navbar data using `children` remains accepted at runtime for migration.
|
||||
|
||||
## API
|
||||
|
||||
The JS module (`@wrnexus/ui`) exposes five helpers used by the build tooling to
|
||||
|
||||
@@ -806,6 +806,69 @@
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "flush",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileCompact",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileMessageLines",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"default": "2",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "hideBadgeOnMobile",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "hideDescriptionOnMobile",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileActionSize",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "gutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "contentGutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "flushMobile",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "size",
|
||||
"type": "string",
|
||||
@@ -2180,6 +2243,48 @@
|
||||
"default": "\"solid\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "spacing",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"none\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "radius",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "titleMaxWidth",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "contentPadding",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobilePadding",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileActions",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"stack\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "primaryLabel",
|
||||
"type": "string",
|
||||
@@ -2315,7 +2420,21 @@
|
||||
"name": "title",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Card title\"",
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "icon",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "titleKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
@@ -2325,6 +2444,13 @@
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "subtitleKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "string",
|
||||
@@ -2332,6 +2458,13 @@
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "descriptionKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "header",
|
||||
"type": "string",
|
||||
@@ -2374,6 +2507,13 @@
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "actionLabelKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "actionHref",
|
||||
"type": "string",
|
||||
@@ -2493,6 +2633,27 @@
|
||||
"default": "\"none\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "radius",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "shadow",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "equalHeight",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "scrollable",
|
||||
"type": "boolean",
|
||||
@@ -5833,6 +5994,34 @@
|
||||
"default": "\"lift\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "radius",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "shadow",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "clickableSurface",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "descriptionTone",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -6198,6 +6387,20 @@
|
||||
"default": "\"compact\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileColumns",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"default": "2",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileCopyrightAlign",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"center\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "copyright",
|
||||
"type": "string",
|
||||
@@ -6205,6 +6408,20 @@
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "copyrightKey",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "translationPrefix",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -8128,6 +8345,97 @@
|
||||
"default": "[]",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "rail",
|
||||
"type": "unknown[]",
|
||||
"required": false,
|
||||
"default": "[]",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "featured",
|
||||
"type": "Record<string, unknown>",
|
||||
"required": false,
|
||||
"default": "{}",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "variant",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "density",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "panelWidth",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"xl\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "panelAlign",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"start\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "columnCount",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"default": "0",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "fullWidth",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "openOnHover",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "activeCategory",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "actionLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "actionHref",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "actionIcon",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"icon-[lucide--arrow-right]\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "footer",
|
||||
"type": "string",
|
||||
@@ -8163,9 +8471,13 @@
|
||||
{
|
||||
"name": "select",
|
||||
"payloadType": "{ item: object; value: string }"
|
||||
},
|
||||
{
|
||||
"name": "category",
|
||||
"payloadType": "{ item: object; value: string; index: number }"
|
||||
}
|
||||
],
|
||||
"events": ["open", "close", "select"],
|
||||
"events": ["open", "close", "select", "category"],
|
||||
"source": "components/MegaMenu.wrn"
|
||||
},
|
||||
{
|
||||
@@ -8878,6 +9190,20 @@
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "floating",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "startFloating",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"0px\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "openOnHover",
|
||||
"type": "boolean",
|
||||
@@ -8899,6 +9225,55 @@
|
||||
"default": "\"Toggle navigation\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "collapseAt",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"900px\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobilePanel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"content\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileUtilitiesPosition",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"header\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "gutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "contentGutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "flushMobile",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "translationPrefix",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -8907,7 +9282,7 @@
|
||||
"options": []
|
||||
}
|
||||
],
|
||||
"slots": ["topbar", "actions"],
|
||||
"slots": ["topbar", "actions", "utilities"],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "toggle",
|
||||
@@ -9826,6 +10201,55 @@
|
||||
"default": "true",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "iconOnly",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "themeIcon",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"icon-[lucide--sun-moon]\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "colorIcon",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"icon-[lucide--palette]\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "languageIcon",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"icon-[lucide--languages]\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "panelPlacement",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"bottom-end\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobilePanel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"dropdown\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "showCurrentLanguage",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -10532,6 +10956,34 @@
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "sticky",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "topOffset",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"6rem\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "sectionOffset",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"7rem\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "collapseAt",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"800px\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -11346,6 +11798,62 @@
|
||||
"default": "true",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "gutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "contentGutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "flushMobile",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "false",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "titleSize",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "titleMaxWidth",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "descriptionWeight",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileTitleSize",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileStack",
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"default": "true",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "backgroundImage",
|
||||
"type": "string",
|
||||
@@ -11498,6 +12006,41 @@
|
||||
"default": "\"xl\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "radius",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "shadow",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"default\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "gutter",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"md\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "outerSpacing",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"none\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "mobileColumns",
|
||||
"type": "number",
|
||||
"required": false,
|
||||
"default": "1",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
@@ -11844,6 +12387,20 @@
|
||||
"default": "\"\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "showLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Show password\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "hideLabel",
|
||||
"type": "string",
|
||||
"required": false,
|
||||
"default": "\"Hide password\"",
|
||||
"options": []
|
||||
},
|
||||
{
|
||||
"name": "class",
|
||||
"type": "string",
|
||||
|
||||
@@ -16,6 +16,15 @@ badge: string = ""
|
||||
dismissLabel: string = "Dismiss announcement"
|
||||
sticky: boolean = false
|
||||
compact: boolean = false
|
||||
flush: boolean = false
|
||||
mobileCompact: boolean = false
|
||||
mobileMessageLines: number = 2
|
||||
hideBadgeOnMobile: boolean = false
|
||||
hideDescriptionOnMobile: boolean = false
|
||||
mobileActionSize: string = "default"
|
||||
gutter: string = "md"
|
||||
contentGutter: string = "md"
|
||||
flushMobile: boolean = false
|
||||
size: string = "default"
|
||||
width: string = "default"
|
||||
color: string = "primary"
|
||||
@@ -36,6 +45,7 @@ badge: string = ""
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-compact='{compact}'
|
||||
data-flush='{flush ? "true" : "false"}' data-mobile-compact='{mobileCompact ? "true" : "false"}' data-mobile-message-lines='{mobileMessageLines}' data-hide-badge-mobile='{hideBadgeOnMobile ? "true" : "false"}' data-hide-description-mobile='{hideDescriptionOnMobile ? "true" : "false"}' data-mobile-action-size='{mobileActionSize}' data-gutter='{gutter}' data-content-gutter='{contentGutter}' data-flush-mobile='{flushMobile ? "true" : "false"}'
|
||||
data-sticky='{sticky}'
|
||||
data-show='{!dismissed}'
|
||||
aria-hidden='{dismissed ? "true" : "false"}'
|
||||
@@ -152,9 +162,9 @@ badge: string = ""
|
||||
position: relative;
|
||||
z-index: 40;
|
||||
display: block;
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
margin-inline: calc(50% - 50vw);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-inline: 0;
|
||||
color: var(--wrn-color-text);
|
||||
}
|
||||
|
||||
@@ -163,6 +173,8 @@ badge: string = ""
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wrn-announcement[data-flush="true"] .wrn-announcement__surface { border-radius: 0; box-shadow: none; }
|
||||
|
||||
.wrn-announcement[data-width="wide"] .wrn-announcement__surface {
|
||||
width: min(calc(100% - 2rem), 90rem);
|
||||
}
|
||||
@@ -573,6 +585,15 @@ badge: string = ""
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wrn-announcement[data-mobile-compact="true"] .wrn-announcement__inner,
|
||||
.wrn-announcement[data-compact="true"] .wrn-announcement__inner { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 0.65rem; }
|
||||
.wrn-announcement[data-hide-badge-mobile="true"] .wrn-announcement__badge,
|
||||
.wrn-announcement[data-mobile-compact="true"] .wrn-announcement__badge { display: none; }
|
||||
.wrn-announcement[data-hide-description-mobile="true"] .wrn-announcement__description,
|
||||
.wrn-announcement[data-mobile-compact="true"] .wrn-announcement__description { display: none; }
|
||||
.wrn-announcement[data-mobile-message-lines="2"] .wrn-announcement__message { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
.wrn-announcement[data-mobile-action-size="sm"] .wrn-announcement__action { min-height: 2.25rem; padding-inline: 0.7rem; }
|
||||
.wrn-announcement[data-flush-mobile="false"] .wrn-announcement__inner { padding-inline: 1rem; }
|
||||
.wrn-announcement__surface,
|
||||
.wrn-announcement[data-width="wide"] .wrn-announcement__surface {
|
||||
width: calc(100% - 1rem);
|
||||
|
||||
@@ -120,11 +120,11 @@ component BackToTop {
|
||||
}
|
||||
|
||||
.wrn-back-to-top[data-position="right"] {
|
||||
right: 1.5rem;
|
||||
right: max(1.5rem, env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
.wrn-back-to-top[data-position="left"] {
|
||||
left: 1.5rem;
|
||||
left: max(1.5rem, env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.wrn-back-to-top[data-position="center"] {
|
||||
|
||||
@@ -12,6 +12,7 @@ component Blockquote {
|
||||
variant: string = "default"
|
||||
quoteMark: boolean = true
|
||||
italic: boolean = true
|
||||
maxWidth: string = "xl"
|
||||
class: string = ""
|
||||
}
|
||||
|
||||
@@ -24,6 +25,7 @@ component Blockquote {
|
||||
data-align="{align}"
|
||||
data-variant="{variant}"
|
||||
data-italic="{italic}"
|
||||
data-max-width="{maxWidth}"
|
||||
>
|
||||
<blockquote cite="{citationUrl}">
|
||||
{#if quoteMark}
|
||||
@@ -73,10 +75,17 @@ component Blockquote {
|
||||
display: grid;
|
||||
gap: 1.25rem;
|
||||
width: 100%;
|
||||
max-width: var(--wrn-blockquote-max-width, 48rem);
|
||||
color: var(--wrn-color-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-max-width="sm"] { --wrn-blockquote-max-width: 32rem; }
|
||||
.wrn-next--blockquote[data-max-width="md"] { --wrn-blockquote-max-width: 40rem; }
|
||||
.wrn-next--blockquote[data-max-width="lg"] { --wrn-blockquote-max-width: 48rem; }
|
||||
.wrn-next--blockquote[data-max-width="xl"] { --wrn-blockquote-max-width: 64rem; }
|
||||
.wrn-next--blockquote[data-max-width="full"] { --wrn-blockquote-max-width: 100%; }
|
||||
|
||||
.wrn-next--blockquote[data-size="xs"] {
|
||||
--wrn-blockquote-font-size: 0.875rem;
|
||||
--wrn-blockquote-mark-size: 3rem;
|
||||
@@ -189,8 +198,19 @@ component Blockquote {
|
||||
|
||||
.wrn-next--blockquote[data-align="center"] {
|
||||
text-align: center;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.wrn-next--blockquote:is([data-variant="contained"], [data-variant="spotlight"], [data-variant="card"]) {
|
||||
padding: clamp(1.5rem, 4vw, 3rem);
|
||||
border: 1px solid color-mix(in srgb, var(--wrn-blockquote-color) 24%, var(--wrn-color-border));
|
||||
border-radius: calc(var(--wrn-radius) * 2);
|
||||
background: linear-gradient(145deg, color-mix(in srgb, var(--wrn-blockquote-color) 8%, var(--wrn-color-surface-raised)), var(--wrn-color-surface-raised));
|
||||
box-shadow: var(--wrn-shadow-1);
|
||||
}
|
||||
|
||||
.wrn-next--blockquote[data-variant="spotlight"] > blockquote { padding-top: calc(var(--wrn-blockquote-mark-size) * 0.55); }
|
||||
|
||||
.wrn-next--blockquote[data-align="right"],
|
||||
.wrn-next--blockquote[data-align="end"] {
|
||||
text-align: right;
|
||||
|
||||
@@ -7,6 +7,7 @@ component Breadcrumb {
|
||||
label: string = "Breadcrumb"
|
||||
items: unknown[] = []
|
||||
active: string = ""
|
||||
activeIcon: string = "icon-[lucide--file-text]"
|
||||
separator: string = "chevron"
|
||||
showHome: boolean = false
|
||||
homeLabel: string = "Home"
|
||||
@@ -61,7 +62,7 @@ label: string = "Breadcrumb"
|
||||
{:else if separator === "arrow"}
|
||||
<span>→</span>
|
||||
{:else}
|
||||
<span class="icon-[lucide--chevron-right] wrn-breadcrumb__separator-icon"></span>
|
||||
<span class="wrn-breadcrumb__separator-icon">›</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -99,6 +100,30 @@ label: string = "Breadcrumb"
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
{#if active && !items.some((item) => active === (item.value || item.label || item.title))}
|
||||
<li class="wrn-breadcrumb__item" data-current="true">
|
||||
{#if showHome || items.length > 0}
|
||||
<span class="wrn-breadcrumb__separator" aria-hidden="true">
|
||||
{#if separator === "slash"}
|
||||
<span>/</span>
|
||||
{:else if separator === "dot"}
|
||||
<span>•</span>
|
||||
{:else if separator === "arrow"}
|
||||
<span>→</span>
|
||||
{:else}
|
||||
<span class="wrn-breadcrumb__separator-icon">›</span>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="wrn-breadcrumb__current" aria-current="page">
|
||||
{#if activeIcon}
|
||||
<span class='{activeIcon} wrn-breadcrumb__icon' aria-hidden="true"></span>
|
||||
{/if}
|
||||
<span class="wrn-breadcrumb__label">{active}</span>
|
||||
</span>
|
||||
</li>
|
||||
{/if}
|
||||
</ol>
|
||||
</nav>
|
||||
}
|
||||
@@ -207,8 +232,14 @@ label: string = "Breadcrumb"
|
||||
}
|
||||
|
||||
.wrn-breadcrumb__separator-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 0.9rem;
|
||||
height: 0.9rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wrn-breadcrumb__link,
|
||||
|
||||
@@ -8,6 +8,12 @@ component CTASection {
|
||||
size: string = "default"
|
||||
color: string = "primary"
|
||||
variant: string = "solid"
|
||||
spacing: string = "none"
|
||||
radius: string = "default"
|
||||
titleMaxWidth: string = ""
|
||||
contentPadding: string = "default"
|
||||
mobilePadding: string = "default"
|
||||
mobileActions: string = "stack"
|
||||
primaryLabel: string = "Get started"
|
||||
primaryHref: string = "#"
|
||||
primaryIcon: string = ""
|
||||
@@ -35,6 +41,9 @@ component CTASection {
|
||||
data-size='{size}'
|
||||
data-color='{color}'
|
||||
data-variant='{variant}'
|
||||
data-spacing='{spacing}'
|
||||
data-radius='{radius}' data-content-padding='{contentPadding}' data-mobile-padding='{mobilePadding}' data-mobile-actions='{mobileActions}'
|
||||
style='--wrn-cta-title-max-width: {titleMaxWidth || "18ch"}'
|
||||
data-max-width='{maxWidth}'
|
||||
data-full-bleed='{fullBleed ? "true" : "false"}'
|
||||
data-visual-position='{visualPosition}'
|
||||
@@ -186,6 +195,18 @@ component CTASection {
|
||||
--cta-contrast: var(--wrn-color-secondary-contrast);
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-spacing="sm"] {
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-spacing="md"] {
|
||||
padding-block: 3rem;
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-spacing="lg"] {
|
||||
padding-block: 4rem;
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-color="info"] {
|
||||
--cta-accent: var(--wrn-color-info);
|
||||
--cta-accent-hover: var(--wrn-color-info-hover);
|
||||
@@ -223,6 +244,20 @@ component CTASection {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.wrn-cta-section[data-spacing="sm"] {
|
||||
padding-block: 1.5rem;
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-spacing="md"] {
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-spacing="lg"] {
|
||||
padding-block: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.wrn-cta-section[data-max-width="compact"] .wrn-cta-section__outer {
|
||||
width: min(calc(100% - 2rem), 64rem);
|
||||
}
|
||||
@@ -260,6 +295,8 @@ component CTASection {
|
||||
0 1px 0 color-mix(in srgb, white 5%, transparent) inset,
|
||||
0 24px 70px color-mix(in srgb, black 15%, transparent);
|
||||
}
|
||||
.wrn-cta-section[data-radius="xl"] .wrn-cta-section__surface { border-radius: 1.5rem; }
|
||||
.wrn-cta-section[data-content-padding="lg"] .wrn-cta-section__surface { padding: 3.5rem clamp(1.5rem, 5vw, 4rem); }
|
||||
|
||||
.wrn-cta-section[data-size="compact"] .wrn-cta-section__surface,
|
||||
.wrn-cta-section[data-size="sm"] .wrn-cta-section__surface {
|
||||
@@ -401,7 +438,7 @@ component CTASection {
|
||||
}
|
||||
|
||||
.wrn-cta-section__title {
|
||||
max-width: 18ch;
|
||||
max-width: var(--wrn-cta-title-max-width);
|
||||
margin: 0.75rem 0 0;
|
||||
color: var(--wrn-color-text);
|
||||
font-size: clamp(2rem, 5vw, 4rem);
|
||||
@@ -637,11 +674,11 @@ component CTASection {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.wrn-cta-section__footer:empty {
|
||||
.wrn-cta-section__footer:not(:has(> *)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wrn-cta-section__footer:not(:empty) {
|
||||
.wrn-cta-section__footer:has(> *) {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.25rem;
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
|
||||
@@ -671,6 +708,8 @@ component CTASection {
|
||||
}
|
||||
|
||||
@media (max-width: 639px) {
|
||||
.wrn-cta-section[data-mobile-padding="md"] .wrn-cta-section__surface { padding: 2.5rem 1.25rem; }
|
||||
.wrn-cta-section[data-mobile-actions="inline"] .wrn-cta-section__action { width: auto; }
|
||||
.wrn-cta-section__surface {
|
||||
padding: 1.6rem;
|
||||
border-radius: 1.2rem;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user