← back to Dw Nextjs Admin Login
initial scaffold (gitify-all 2026-05-06)
8acca81dc8e643cf0d6cb16d95c1a90a020f8c26 · 2026-05-06 10:25:15 -0700 · Steve Abrams
Files touched
A .gitignoreA package-lock.jsonA package.jsonA src/index.tsA test/auth.test.tsA tsconfig.build.jsonA tsconfig.json
Diff
commit 8acca81dc8e643cf0d6cb16d95c1a90a020f8c26
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 6 10:25:15 2026 -0700
initial scaffold (gitify-all 2026-05-06)
---
.gitignore | 12 +
package-lock.json | 1540 +++++++++++++++++++++++++++++++++++++++++++++++++++
package.json | 24 +
src/index.ts | 451 +++++++++++++++
test/auth.test.ts | 284 ++++++++++
tsconfig.build.json | 11 +
tsconfig.json | 23 +
7 files changed, 2345 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7e6a9c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+node_modules/
+.env
+.env.local
+.env.*.local
+.env.*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
+*.bak
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..27b41e7
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1540 @@
+{
+ "name": "@dw/nextjs-admin-login",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@dw/nextjs-admin-login",
+ "version": "0.1.0",
+ "devDependencies": {
+ "@types/node": "^22",
+ "tsx": "^4",
+ "typescript": "^5"
+ },
+ "peerDependencies": {
+ "next": "^15.0.0 || ^16.0.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.7.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@next/env": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.4.tgz",
+ "integrity": "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@next/swc-darwin-arm64": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.4.tgz",
+ "integrity": "sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-darwin-x64": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.4.tgz",
+ "integrity": "sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-gnu": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.4.tgz",
+ "integrity": "sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-arm64-musl": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.4.tgz",
+ "integrity": "sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-gnu": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.4.tgz",
+ "integrity": "sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-linux-x64-musl": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.4.tgz",
+ "integrity": "sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-arm64-msvc": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.4.tgz",
+ "integrity": "sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@next/swc-win32-x64-msvc": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.4.tgz",
+ "integrity": "sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.15",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
+ "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "22.19.17",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
+ "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.27",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.27.tgz",
+ "integrity": "sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==",
+ "license": "Apache-2.0",
+ "peer": true,
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001791",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz",
+ "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0",
+ "peer": true
+ },
+ "node_modules/client-only": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.12",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/next": {
+ "version": "16.2.4",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.4.tgz",
+ "integrity": "sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@next/env": "16.2.4",
+ "@swc/helpers": "0.5.15",
+ "baseline-browser-mapping": "^2.9.19",
+ "caniuse-lite": "^1.0.30001579",
+ "postcss": "8.4.31",
+ "styled-jsx": "5.1.6"
+ },
+ "bin": {
+ "next": "dist/bin/next"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "optionalDependencies": {
+ "@next/swc-darwin-arm64": "16.2.4",
+ "@next/swc-darwin-x64": "16.2.4",
+ "@next/swc-linux-arm64-gnu": "16.2.4",
+ "@next/swc-linux-arm64-musl": "16.2.4",
+ "@next/swc-linux-x64-gnu": "16.2.4",
+ "@next/swc-linux-x64-musl": "16.2.4",
+ "@next/swc-win32-arm64-msvc": "16.2.4",
+ "@next/swc-win32-x64-msvc": "16.2.4",
+ "sharp": "^0.34.5"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.1.0",
+ "@playwright/test": "^1.51.1",
+ "babel-plugin-react-compiler": "*",
+ "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
+ "sass": "^1.3.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@playwright/test": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC",
+ "peer": true
+ },
+ "node_modules/postcss": {
+ "version": "8.4.31",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+ "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "nanoid": "^3.3.6",
+ "picocolors": "^1.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.5"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@img/colour": "^1.0.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.7.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/styled-jsx": {
+ "version": "5.1.6",
+ "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
+ "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "client-only": "0.0.1"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-plugin-macros": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "peer": true
+ },
+ "node_modules/tsx": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
+ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "~0.27.0",
+ "get-tsconfig": "^4.7.5"
+ },
+ "bin": {
+ "tsx": "dist/cli.mjs"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bbab1de
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "@dw/nextjs-admin-login",
+ "version": "0.2.0",
+ "private": true,
+ "type": "module",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": ["dist", "src"],
+ "exports": {
+ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
+ },
+ "scripts": {
+ "build": "tsc -p tsconfig.build.json",
+ "test": "node --import tsx --test test/*.test.ts",
+ "typecheck": "tsc --noEmit",
+ "prepare": "npm run build"
+ },
+ "peerDependencies": { "next": "^15.0.0 || ^16.0.0" },
+ "devDependencies": {
+ "@types/node": "^22",
+ "tsx": "^4",
+ "typescript": "^5"
+ }
+}
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..02a6763
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,451 @@
+/**
+ * @dw/nextjs-admin-login
+ *
+ * Shared admin auth for DW Next.js verticals (Freddy / Grant / Hub / Patty / PoppyPetitions).
+ * Collapses 5 byte-identical lib/auth.ts files + 5 near-identical login route handlers.
+ *
+ * Token format: base64(username:timestamp:sha256(username:timestamp:secret))
+ * This is the canonical format already live across all 5 verticals — existing sessions
+ * remain valid after consumers adopt this package.
+ */
+
+import { createHash, timingSafeEqual } from 'node:crypto';
+
+// ---------------------------------------------------------------------------
+// Public types
+// ---------------------------------------------------------------------------
+
+export interface AuthConfig {
+ /** Required. Cookie name, e.g. 'freddy-auth'. */
+ cookieName: string;
+ /** Env var holding the session secret. Default: 'SESSION_SECRET'. */
+ sessionSecretEnv?: string;
+ /** Env var holding the expected username. Default: 'AUTH_USERNAME'. */
+ usernameEnv?: string;
+ /** Env var holding the expected password. Default: 'AUTH_PASSWORD'. */
+ passwordEnv?: string;
+ /** Default username when usernameEnv is unset in env. Default: 'admin'. */
+ defaultUsername?: string;
+ /** Session lifetime in seconds. Default: 86400 (24 h). */
+ maxAgeSeconds?: number;
+ /**
+ * Accept Authorization: Basic <b64(user:pass)> in addition to cookie auth.
+ * Only enable for Poppy / API-to-API callers. Default: false.
+ */
+ enableBasicAuthFallback?: boolean;
+ /**
+ * Append Secure flag to Set-Cookie when NODE_ENV === 'production'.
+ * Default: true (fixes Freddy regression where the flag was absent).
+ */
+ enableSecureCookie?: boolean;
+ /** Prefix for console.error / console.log lines. Default: '[auth/login]'. */
+ logTag?: string;
+}
+
+/**
+ * 2026-05-05 (v0.2.0): payload shape for org-scoped sessions.
+ * orgId is null for legacy 3-part tokens minted before v0.2.0.
+ */
+export interface AuthSession {
+ username: string;
+ orgId: string | null;
+}
+
+export interface AuthHelpers {
+ /** The cookie name passed to createAuth. */
+ readonly COOKIE_NAME: string;
+ /**
+ * Mint a new signed session token for username.
+ * Format: base64(username:timestamp:sha256(username:timestamp:secret))
+ */
+ createSession(username: string): string;
+ /**
+ * Validate a cookie/header on an incoming request.
+ * Returns the authenticated username, or null if invalid / expired.
+ */
+ verifyAuth(req: Request): string | null;
+ /** Build the Set-Cookie header value for a new session. */
+ buildAuthCookie(token: string): string;
+ /** Build the Set-Cookie header value that clears the session (logout). */
+ buildLogoutCookie(): string;
+ /**
+ * SHA-256 hex digest. Matches the hashPassword implementation
+ * used by all 5 verticals (not bcrypt — none of the 5 used it).
+ */
+ hashPassword(pw: string): string;
+ /**
+ * v0.2.0 — Mint a new signed session token that embeds orgId.
+ * Format: base64(username:timestamp:orgId:sha256(username:timestamp:orgId:secret))
+ * Verified by verifyAuthWithOrg. Backward compatible: old verifyAuth
+ * rejects the 4-part format (returns null), so always issue v2 tokens
+ * AFTER the consumer has migrated to verifyAuthWithOrg.
+ */
+ createSessionWithOrg(username: string, orgId: string | null): string;
+ /**
+ * v0.2.0 — Validate a cookie on an incoming request and return both
+ * the username AND the embedded orgId. Accepts both v1 (3-part, returns
+ * orgId: null) and v2 (4-part, returns orgId: string) tokens — so
+ * existing logged-in browsers don't get bumped at upgrade time.
+ * Returns null if invalid / expired.
+ */
+ verifyAuthWithOrg(req: Request): AuthSession | null;
+}
+
+// ---------------------------------------------------------------------------
+// Internal helpers
+// ---------------------------------------------------------------------------
+
+/**
+ * Read an env var at call-time (not module-load time) so tests can set
+ * process.env after import.
+ */
+function env(name: string): string | undefined {
+ return process.env[name];
+}
+
+function hmac(username: string, timestamp: string, secret: string): string {
+ return createHash('sha256')
+ .update(`${username}:${timestamp}:${secret}`)
+ .digest('hex');
+}
+
+// v0.2.0 — HMAC for the org-scoped 4-part token format.
+function hmacWithOrg(
+ username: string,
+ timestamp: string,
+ orgId: string,
+ secret: string,
+): string {
+ return createHash('sha256')
+ .update(`${username}:${timestamp}:${orgId}:${secret}`)
+ .digest('hex');
+}
+
+function constantTimeEqual(a: string, b: string): boolean {
+ const ab = Buffer.from(a, 'utf8');
+ const bb = Buffer.from(b, 'utf8');
+ // Buffers must be the same length for timingSafeEqual.
+ // If lengths differ we still do a comparison against a same-length buffer to
+ // prevent short-circuit timing, then return false.
+ if (ab.length !== bb.length) {
+ timingSafeEqual(ab, ab); // burn time
+ return false;
+ }
+ return timingSafeEqual(ab, bb);
+}
+
+// ---------------------------------------------------------------------------
+// createAuth factory
+// ---------------------------------------------------------------------------
+
+export function createAuth(cfg: AuthConfig): AuthHelpers {
+ const {
+ cookieName,
+ sessionSecretEnv = 'SESSION_SECRET',
+ usernameEnv = 'AUTH_USERNAME',
+ passwordEnv = 'AUTH_PASSWORD',
+ defaultUsername = 'admin',
+ maxAgeSeconds = 86400,
+ enableBasicAuthFallback = false,
+ enableSecureCookie = true,
+ } = cfg;
+
+ // ---- session helpers ----
+
+ function getSecret(): string {
+ const s = env(sessionSecretEnv);
+ if (!s) {
+ throw new Error(
+ `${sessionSecretEnv} env var is required (no fallback for production safety)`,
+ );
+ }
+ return s;
+ }
+
+ function _createSession(username: string): string {
+ const secret = getSecret();
+ const timestamp = Date.now().toString();
+ const sig = hmac(username, timestamp, secret);
+ const payload = `${username}:${timestamp}:${sig}`;
+ return Buffer.from(payload).toString('base64');
+ }
+
+ function _verifySessionToken(token: string): string | null {
+ try {
+ const secret = env(sessionSecretEnv);
+ if (!secret) return null;
+
+ const decoded = Buffer.from(token, 'base64').toString('utf-8');
+ const parts = decoded.split(':');
+ // username may itself contain colons only if someone passed a weird
+ // username, but our createSession always splits into exactly 3 parts
+ // (username, timestamp, 64-char hex sig). Guard strictly.
+ if (parts.length !== 3) return null;
+
+ const [username, timestamp, signature] = parts as [string, string, string];
+
+ const age = Date.now() - parseInt(timestamp, 10);
+ if (isNaN(age) || age > maxAgeSeconds * 1000) return null;
+
+ const expected = hmac(username, timestamp, secret);
+ if (!constantTimeEqual(signature, expected)) return null;
+
+ return username;
+ } catch {
+ return null;
+ }
+ }
+
+ function _verifyAuth(req: Request): string | null {
+ // Basic auth fallback (opt-in, Poppy only)
+ if (enableBasicAuthFallback) {
+ const authHeader = req.headers.get('authorization');
+ if (authHeader?.startsWith('Basic ')) {
+ try {
+ const expectedUser = env(usernameEnv) ?? defaultUsername;
+ const expectedPass = env(passwordEnv);
+ if (expectedPass) {
+ const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8');
+ const colonIdx = decoded.indexOf(':');
+ if (colonIdx !== -1) {
+ const user = decoded.slice(0, colonIdx);
+ const pass = decoded.slice(colonIdx + 1);
+ if (constantTimeEqual(user, expectedUser) && constantTimeEqual(pass, expectedPass)) {
+ return user;
+ }
+ }
+ }
+ } catch {
+ // fall through to cookie auth
+ }
+ }
+ }
+
+ // Cookie auth
+ const cookieHeader = req.headers.get('cookie');
+ if (!cookieHeader) return null;
+
+ const cookies = Object.fromEntries(
+ cookieHeader.split(';').map((c) => {
+ const [key, ...rest] = c.trim().split('=');
+ return [key ?? '', rest.join('=')];
+ }),
+ );
+
+ const token = cookies[cookieName];
+ if (!token) return null;
+
+ return _verifySessionToken(token);
+ }
+
+ function _buildAuthCookie(token: string): string {
+ const useSecure =
+ enableSecureCookie && process.env['NODE_ENV'] === 'production';
+ const parts = [
+ `${cookieName}=${token}`,
+ `Path=/`,
+ `HttpOnly`,
+ `SameSite=Lax`,
+ `Max-Age=${maxAgeSeconds}`,
+ ];
+ if (useSecure) parts.push('Secure');
+ return parts.join('; ');
+ }
+
+ function _buildLogoutCookie(): string {
+ return `${cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
+ }
+
+ function _hashPassword(pw: string): string {
+ return createHash('sha256').update(pw).digest('hex');
+ }
+
+ // -------------------------------------------------------------------------
+ // v0.2.0 — org-scoped session helpers
+ // -------------------------------------------------------------------------
+
+ function _createSessionWithOrg(username: string, orgId: string | null): string {
+ // If orgId is null we deliberately mint a v1 (3-part) token so a future
+ // verifyAuthWithOrg call returns orgId: null — same behavior, smaller
+ // token, no cross-format edge cases. Only emit v2 when orgId is real.
+ if (orgId === null) {
+ return _createSession(username);
+ }
+ const secret = getSecret();
+ const timestamp = Date.now().toString();
+ const sig = hmacWithOrg(username, timestamp, orgId, secret);
+ const payload = `${username}:${timestamp}:${orgId}:${sig}`;
+ return Buffer.from(payload).toString('base64');
+ }
+
+ function _verifySessionTokenWithOrg(token: string): AuthSession | null {
+ try {
+ const secret = env(sessionSecretEnv);
+ if (!secret) return null;
+
+ const decoded = Buffer.from(token, 'base64').toString('utf-8');
+ const parts = decoded.split(':');
+
+ // v1 (3-part): username:timestamp:sig — orgId unknown, return null orgId.
+ if (parts.length === 3) {
+ const username = _verifySessionToken(token);
+ return username ? { username, orgId: null } : null;
+ }
+
+ // v2 (4-part): username:timestamp:orgId:sig
+ if (parts.length !== 4) return null;
+ const [username, timestamp, orgId, signature] = parts as [string, string, string, string];
+
+ const age = Date.now() - parseInt(timestamp, 10);
+ if (isNaN(age) || age > maxAgeSeconds * 1000) return null;
+
+ const expected = hmacWithOrg(username, timestamp, orgId, secret);
+ if (!constantTimeEqual(signature, expected)) return null;
+
+ return { username, orgId };
+ } catch {
+ return null;
+ }
+ }
+
+ function _verifyAuthWithOrg(req: Request): AuthSession | null {
+ // Basic auth fallback (opt-in, Poppy only). Returns no orgId since
+ // basic-auth carries no embedded session payload.
+ if (enableBasicAuthFallback) {
+ const authHeader = req.headers.get('authorization');
+ if (authHeader?.startsWith('Basic ')) {
+ try {
+ const expectedUser = env(usernameEnv) ?? defaultUsername;
+ const expectedPass = env(passwordEnv);
+ if (expectedPass) {
+ const decoded = Buffer.from(authHeader.slice(6), 'base64').toString('utf-8');
+ const colonIdx = decoded.indexOf(':');
+ if (colonIdx !== -1) {
+ const user = decoded.slice(0, colonIdx);
+ const pass = decoded.slice(colonIdx + 1);
+ if (constantTimeEqual(user, expectedUser) && constantTimeEqual(pass, expectedPass)) {
+ return { username: user, orgId: null };
+ }
+ }
+ }
+ } catch {
+ // fall through to cookie auth
+ }
+ }
+ }
+
+ const cookieHeader = req.headers.get('cookie');
+ if (!cookieHeader) return null;
+
+ const cookies = Object.fromEntries(
+ cookieHeader.split(';').map((c) => {
+ const [key, ...rest] = c.trim().split('=');
+ return [key ?? '', rest.join('=')];
+ }),
+ );
+
+ const token = cookies[cookieName];
+ if (!token) return null;
+
+ return _verifySessionTokenWithOrg(token);
+ }
+
+ return {
+ COOKIE_NAME: cookieName,
+ createSession: _createSession,
+ verifyAuth: _verifyAuth,
+ buildAuthCookie: _buildAuthCookie,
+ buildLogoutCookie: _buildLogoutCookie,
+ hashPassword: _hashPassword,
+ createSessionWithOrg: _createSessionWithOrg,
+ verifyAuthWithOrg: _verifyAuthWithOrg,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// createLoginHandler — replaces app/api/auth/login/route.ts in each project
+// ---------------------------------------------------------------------------
+
+/**
+ * NextRequest / NextResponse are peer-dep types from 'next/server'.
+ * We use the global Request/Response interfaces here and cast at the boundary
+ * so this package has no hard runtime dependency on Next.js itself.
+ *
+ * In the consumer's route.ts:
+ * import { NextRequest, NextResponse } from 'next/server';
+ * export const POST = createLoginHandler(helpers, cfg) as (req: NextRequest) => Promise<NextResponse>;
+ */
+type NextRequestLike = Request & {
+ json(): Promise<unknown>;
+};
+
+type NextResponseLike = Response;
+
+interface NextResponseConstructor {
+ json(body: unknown, init?: ResponseInit): NextResponseLike;
+}
+
+/**
+ * Resolve NextResponse at runtime from 'next/server' so we don't
+ * import Next.js at module-load time (keeps the package installable outside a
+ * Next.js project and lets unit tests stub the peer dep via a loader hook).
+ *
+ * Using new Function('specifier', 'return import(specifier)') prevents
+ * TypeScript from statically resolving 'next/server' at compile time, so the
+ * build succeeds even without next installed in this package's node_modules.
+ */
+const dynamicImport = new Function('specifier', 'return import(specifier)') as
+ (s: string) => Promise<unknown>;
+
+async function getNR(): Promise<NextResponseConstructor> {
+ const mod = (await dynamicImport('next/server')) as { NextResponse: NextResponseConstructor };
+ return mod.NextResponse;
+}
+
+export function createLoginHandler(
+ helpers: AuthHelpers,
+ cfg: AuthConfig,
+): (req: Request) => Promise<Response> {
+ const tag = cfg.logTag ?? '[auth/login]';
+ const usernameEnv = cfg.usernameEnv ?? 'AUTH_USERNAME';
+ const passwordEnv = cfg.passwordEnv ?? 'AUTH_PASSWORD';
+ const defaultUsername = cfg.defaultUsername ?? 'admin';
+
+ return async function POST(req: NextRequestLike): Promise<NextResponseLike> {
+ const NR = await getNR();
+ try {
+ const body = (await req.json()) as Record<string, unknown>;
+ const { username, password } = body as { username?: string; password?: string };
+
+ if (!username || !password) {
+ return NR.json({ error: 'Username and password are required' }, { status: 400 });
+ }
+
+ const expectedUsername = process.env[usernameEnv] ?? defaultUsername;
+ const expectedPassword = process.env[passwordEnv];
+
+ if (!expectedPassword) {
+ console.error(`${tag} ${passwordEnv} env unset — refusing all logins (fail-closed)`);
+ return NR.json({ error: 'Server misconfigured' }, { status: 500 });
+ }
+
+ // Constant-time compare for both fields.
+ const userOk = constantTimeEqual(username, expectedUsername);
+ const passOk = constantTimeEqual(password, expectedPassword);
+
+ if (!userOk || !passOk) {
+ return NR.json({ error: 'Invalid credentials' }, { status: 401 });
+ }
+
+ const token = helpers.createSession(username);
+ const cookieValue = helpers.buildAuthCookie(token);
+
+ const response = NR.json({ success: true });
+ response.headers.set('Set-Cookie', cookieValue);
+ return response;
+ } catch (err) {
+ console.error(`${tag} Error:`, (err as Error).message);
+ return NR.json({ error: 'Internal server error' }, { status: 500 });
+ }
+ };
+}
diff --git a/test/auth.test.ts b/test/auth.test.ts
new file mode 100644
index 0000000..53d708b
--- /dev/null
+++ b/test/auth.test.ts
@@ -0,0 +1,284 @@
+/**
+ * Tests for @dw/nextjs-admin-login
+ *
+ * Run with: node --import tsx --test test/*.test.ts
+ *
+ * The 'next/server' peer dep is stubbed via Module.register mock below.
+ * We use Node's built-in test runner (node:test) — no extra deps.
+ */
+
+import { describe, it, before, after, beforeEach } from 'node:test';
+import assert from 'node:assert/strict';
+import { register } from 'node:module';
+import { createHash } from 'node:crypto';
+
+// ---------------------------------------------------------------------------
+// Stub next/server so tests run without a Next.js install
+// ---------------------------------------------------------------------------
+
+// Simple in-process stub: we intercept 'next/server' via a mock loader.
+// Since tsx runs under --import, we can use a manual Module.prototype trick.
+// Easiest portable approach for Node 22: register a custom resolver that
+// returns a synthetic module for 'next/server'.
+
+// We'll inject it into the module cache via a loader hook file.
+// The stub is written to a temp file and registered before importing the source.
+
+import { writeFileSync, unlinkSync, mkdtempSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+
+// Write a CJS-compatible stub loader that replaces next/server
+const stubDir = mkdtempSync(join(tmpdir(), 'dw-login-test-'));
+const stubFile = join(stubDir, 'next-server-stub.mjs');
+
+writeFileSync(
+ stubFile,
+ `
+// Minimal NextResponse stub for unit tests
+class NextResponse extends Response {
+ static json(body, init) {
+ const res = new NextResponse(JSON.stringify(body), {
+ ...init,
+ headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
+ });
+ return res;
+ }
+}
+class NextRequest extends Request {}
+export { NextResponse, NextRequest };
+`,
+);
+
+// Use a loader hook to intercept 'next/server'
+const loaderFile = join(stubDir, 'loader.mjs');
+writeFileSync(
+ loaderFile,
+ `
+const STUB = ${JSON.stringify(stubFile)};
+export async function resolve(specifier, context, nextResolve) {
+ if (specifier === 'next/server') {
+ return { shortCircuit: true, url: 'file://' + STUB };
+ }
+ return nextResolve(specifier, context);
+}
+export async function load(url, context, nextLoad) {
+ return nextLoad(url, context);
+}
+`,
+);
+
+// Register the loader — must happen before importing the module under test
+register('file://' + loaderFile);
+
+// Now import the module under test
+const { createAuth, createLoginHandler } = await import('../src/index.ts');
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function makeRequest(opts: {
+ cookie?: string;
+ authHeader?: string;
+ body?: unknown;
+ method?: string;
+}): Request {
+ const headers = new Headers();
+ if (opts.cookie) headers.set('cookie', opts.cookie);
+ if (opts.authHeader) headers.set('authorization', opts.authHeader);
+ const init: RequestInit = {
+ method: opts.method ?? 'GET',
+ headers,
+ };
+ if (opts.body) init.body = JSON.stringify(opts.body);
+ return new Request('http://localhost/api/auth/login', init);
+}
+
+function savedEnv(keys: string[]): Record<string, string | undefined> {
+ return Object.fromEntries(keys.map((k) => [k, process.env[k]]));
+}
+function restoreEnv(saved: Record<string, string | undefined>): void {
+ for (const [k, v] of Object.entries(saved)) {
+ if (v === undefined) delete process.env[k];
+ else process.env[k] = v;
+ }
+}
+
+const ENV_KEYS = ['SESSION_SECRET', 'AUTH_USERNAME', 'AUTH_PASSWORD', 'NODE_ENV'];
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+describe('@dw/nextjs-admin-login', () => {
+ let saved: Record<string, string | undefined>;
+
+ before(() => {
+ saved = savedEnv(ENV_KEYS);
+ });
+
+ after(() => {
+ restoreEnv(saved);
+ // Cleanup temp stubs
+ try { unlinkSync(stubFile); } catch { /* ok */ }
+ try { unlinkSync(loaderFile); } catch { /* ok */ }
+ });
+
+ beforeEach(() => {
+ // Reset to a known state for each test
+ process.env['SESSION_SECRET'] = 'test-secret-for-unit-tests-32bytes!!';
+ process.env['AUTH_USERNAME'] = 'admin';
+ process.env['AUTH_PASSWORD'] = 'correct-password';
+ process.env['NODE_ENV'] = 'test';
+ });
+
+ // Test 1: createAuth with default config
+ it('createAuth: produces helpers; COOKIE_NAME matches cookieName input', () => {
+ const helpers = createAuth({ cookieName: 'freddy-auth' });
+ assert.equal(helpers.COOKIE_NAME, 'freddy-auth');
+ assert.equal(typeof helpers.createSession, 'function');
+ assert.equal(typeof helpers.verifyAuth, 'function');
+ assert.equal(typeof helpers.buildAuthCookie, 'function');
+ assert.equal(typeof helpers.buildLogoutCookie, 'function');
+ assert.equal(typeof helpers.hashPassword, 'function');
+ });
+
+ // Test 2: createSession round-trip — verifyAuth returns same username
+ it('createSession round-trip: verifyAuth returns original username', () => {
+ const helpers = createAuth({ cookieName: 'test-auth' });
+ const token = helpers.createSession('admin');
+ const cookieHeader = `test-auth=${token}`;
+ const req = makeRequest({ cookie: cookieHeader });
+ const result = helpers.verifyAuth(req);
+ assert.equal(result, 'admin');
+ });
+
+ // Test 3: Mismatched HMAC rejected
+ it('verifyAuth: rejects token with tampered signature', () => {
+ const helpers = createAuth({ cookieName: 'test-auth' });
+ const token = helpers.createSession('admin');
+ // Decode, corrupt last byte of signature, re-encode
+ const decoded = Buffer.from(token, 'base64').toString('utf-8');
+ const parts = decoded.split(':');
+ // parts[2] is the 64-char hex signature — flip last char
+ const sig = parts[2]!;
+ const corrupted = sig.slice(0, -1) + (sig.endsWith('a') ? 'b' : 'a');
+ parts[2] = corrupted;
+ const badToken = Buffer.from(parts.join(':')).toString('base64');
+
+ const req = makeRequest({ cookie: `test-auth=${badToken}` });
+ const result = helpers.verifyAuth(req);
+ assert.equal(result, null);
+ });
+
+ // Test 4: Expired token rejected
+ it('verifyAuth: rejects token older than maxAgeSeconds', () => {
+ const helpers = createAuth({ cookieName: 'test-auth', maxAgeSeconds: 1 });
+
+ // Manually mint a token with a timestamp 2 seconds in the past
+ const secret = process.env['SESSION_SECRET']!;
+ const username = 'admin';
+ const timestamp = (Date.now() - 2000).toString(); // 2s ago, maxAge=1s
+ const sig = createHash('sha256').update(`${username}:${timestamp}:${secret}`).digest('hex');
+ const payload = `${username}:${timestamp}:${sig}`;
+ const expiredToken = Buffer.from(payload).toString('base64');
+
+ const req = makeRequest({ cookie: `test-auth=${expiredToken}` });
+ const result = helpers.verifyAuth(req);
+ assert.equal(result, null);
+ });
+
+ // Test 5: Login handler — env unset → 500
+ it('createLoginHandler: AUTH_PASSWORD unset returns 500', async () => {
+ delete process.env['AUTH_PASSWORD'];
+ const helpers = createAuth({ cookieName: 'test-auth' });
+ const handler = createLoginHandler(helpers, { cookieName: 'test-auth' });
+ const req = makeRequest({ method: 'POST', body: { username: 'admin', password: 'x' } });
+ const res = await handler(req);
+ assert.equal(res.status, 500);
+ const body = await res.json() as { error: string };
+ assert.match(body.error, /misconfigured/i);
+ });
+
+ // Test 6: Login handler — bad password → 401
+ it('createLoginHandler: wrong password returns 401', async () => {
+ const helpers = createAuth({ cookieName: 'test-auth' });
+ const handler = createLoginHandler(helpers, { cookieName: 'test-auth' });
+ const req = makeRequest({ method: 'POST', body: { username: 'admin', password: 'wrong' } });
+ const res = await handler(req);
+ assert.equal(res.status, 401);
+ });
+
+ // Test 7: Login handler — good password → 200 + Set-Cookie
+ it('createLoginHandler: correct credentials returns 200 + Set-Cookie', async () => {
+ const helpers = createAuth({ cookieName: 'test-auth' });
+ const handler = createLoginHandler(helpers, { cookieName: 'test-auth' });
+ const req = makeRequest({
+ method: 'POST',
+ body: { username: 'admin', password: 'correct-password' },
+ });
+ const res = await handler(req);
+ assert.equal(res.status, 200);
+ const setCookie = res.headers.get('set-cookie');
+ assert.ok(setCookie, 'Set-Cookie header should be present');
+ assert.ok(setCookie.startsWith('test-auth='), 'Cookie should start with the cookie name');
+ assert.ok(setCookie.includes('HttpOnly'), 'Cookie should be HttpOnly');
+ });
+
+ // Test 8: Login handler with enableBasicAuthFallback — Basic header parsed
+ it('createLoginHandler + verifyAuth: Basic auth fallback accepted when enabled', () => {
+ const helpers = createAuth({
+ cookieName: 'poppy-auth',
+ enableBasicAuthFallback: true,
+ });
+
+ const b64 = Buffer.from('admin:correct-password').toString('base64');
+ const req = makeRequest({ authHeader: `Basic ${b64}` });
+ const result = helpers.verifyAuth(req);
+ assert.equal(result, 'admin');
+ });
+
+ // Bonus: Basic auth ignored when enableBasicAuthFallback is false
+ it('verifyAuth: Basic header ignored when enableBasicAuthFallback is false (default)', () => {
+ const helpers = createAuth({ cookieName: 'freddy-auth' }); // default = false
+ const b64 = Buffer.from('admin:correct-password').toString('base64');
+ const req = makeRequest({ authHeader: `Basic ${b64}` });
+ const result = helpers.verifyAuth(req);
+ assert.equal(result, null);
+ });
+
+ // Bonus: buildAuthCookie contains Secure in production
+ it('buildAuthCookie: includes Secure flag when NODE_ENV=production and enableSecureCookie=true', () => {
+ process.env['NODE_ENV'] = 'production';
+ const helpers = createAuth({ cookieName: 'freddy-auth', enableSecureCookie: true });
+ const cookie = helpers.buildAuthCookie('sometoken');
+ assert.ok(cookie.includes('Secure'), 'Should include Secure flag in production');
+ });
+
+ // Bonus: buildAuthCookie omits Secure when enableSecureCookie=false
+ it('buildAuthCookie: omits Secure flag when enableSecureCookie=false', () => {
+ process.env['NODE_ENV'] = 'production';
+ const helpers = createAuth({ cookieName: 'freddy-auth', enableSecureCookie: false });
+ const cookie = helpers.buildAuthCookie('sometoken');
+ assert.ok(!cookie.includes('Secure'), 'Should not include Secure flag when disabled');
+ });
+
+ // Bonus: buildLogoutCookie sets Max-Age=0
+ it('buildLogoutCookie: sets Max-Age=0', () => {
+ const helpers = createAuth({ cookieName: 'freddy-auth' });
+ const cookie = helpers.buildLogoutCookie();
+ assert.ok(cookie.includes('Max-Age=0'), 'Logout cookie should have Max-Age=0');
+ assert.ok(cookie.startsWith('freddy-auth=;'), 'Should clear cookie by name');
+ });
+
+ // Bonus: hashPassword is deterministic SHA-256
+ it('hashPassword: returns deterministic SHA-256 hex digest', () => {
+ const helpers = createAuth({ cookieName: 'test-auth' });
+ const h1 = helpers.hashPassword('secret');
+ const h2 = helpers.hashPassword('secret');
+ assert.equal(h1, h2);
+ assert.equal(h1.length, 64); // 256-bit = 64 hex chars
+ assert.notEqual(helpers.hashPassword('other'), h1);
+ });
+});
diff --git a/tsconfig.build.json b/tsconfig.build.json
new file mode 100644
index 0000000..0a18703
--- /dev/null
+++ b/tsconfig.build.json
@@ -0,0 +1,11 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "rootDir": "src",
+ "outDir": "dist",
+ "noEmit": false,
+ "allowImportingTsExtensions": false
+ },
+ "include": ["src"],
+ "exclude": ["node_modules", "dist", "test"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..5e59e33
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ES2022"],
+ "strict": true,
+ "exactOptionalPropertyTypes": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "forceConsistentCasingInFileNames": true,
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "rootDir": ".",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src", "test"],
+ "exclude": ["node_modules", "dist"]
+}
(oldest)
·
back to Dw Nextjs Admin Login
·
v0.3.0: add config-driven capability layer (guest tiers + mi e82caf0 →