diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 93cc892..2a8fb43 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,8 +14,6 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- with:
- bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun run format:check
@@ -24,8 +22,6 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- with:
- bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun run lint:check
@@ -34,8 +30,6 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- with:
- bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun run test:run
@@ -44,7 +38,5 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
- with:
- bun-version: 1.4.2
- run: bun install --frozen-lockfile
- run: bun run typecheck
diff --git a/README.md b/README.md
index a5b9c59..859aa1c 100644
--- a/README.md
+++ b/README.md
@@ -15,11 +15,12 @@ Example names combine the authentication flow and storage, with a transport suff
| Flow | TanStack Start | Next.js | Bun | Convex |
| --- | --- | --- | --- | --- |
-| `otp` | [memory](examples/tanstack-start-react/otp-memory), postgres (planned) | [memory](examples/nextjs/otp-memory) | [memory-cookie](examples/bun-react/otp-memory-cookie), [memory-header](examples/bun-react/otp-memory-header) | [Convex](examples/convex-react/otp) |
+| `otp` | postgres (planned) | [memory](examples/nextjs/otp-memory) | [memory-cookie](examples/bun-react/otp-memory-cookie), [memory-header](examples/bun-react/otp-memory-header) | [Convex](examples/convex-react/otp) |
| `passkey` | [memory](examples/tanstack-start-react/passkey-memory), postgres (planned) | — | — | — |
| `otp-passkey` | [memory](examples/tanstack-start-react/otp-passkey-memory), postgres (planned) | — | — | — |
| `otp-passkey-strict` | [memory](examples/tanstack-start-react/otp-passkey-strict-memory), postgres (planned) | — | — | — |
| `passkey-otp` | [memory](examples/tanstack-start-react/passkey-otp-memory), postgres (planned) | — | — | — |
+| `kitchen-sink` | [memory](examples/tanstack-start-react/kitchen-sink) | — | — | — |
Memory storage is for demos. Its state is local to each server instance and is lost on restart.
diff --git a/bun.lock b/bun.lock
index a0b993e..f2d5227 100644
--- a/bun.lock
+++ b/bun.lock
@@ -8,7 +8,7 @@
"prettier": "^3.9.9",
"prettier-plugin-tailwindcss": "^0.8.1",
"typescript": "^6.0.3",
- "vitest": "^5.0.2",
+ "vitest": "^5.0.3",
},
},
"examples/bun-react/otp-memory-cookie": {
@@ -47,6 +47,16 @@
"@types/react-dom": "^19.3.0",
},
},
+ "examples/bun/otp": {
+ "name": "@repo/example-bun-spike-otp",
+ "dependencies": {
+ "@repo/spike": "workspace:*",
+ },
+ "devDependencies": {
+ "@types/bun": "^1.4.2",
+ "typescript": "^6.0.3",
+ },
+ },
"examples/convex-react/otp": {
"name": "@repo/example-convex-otp",
"dependencies": {
@@ -64,7 +74,7 @@
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1",
+ "vite": "^8.3.2",
},
},
"examples/nextjs/otp-memory": {
@@ -73,14 +83,14 @@
"dependencies": {
"@repo/shared-react": "workspace:*",
"authax": "workspace:*",
- "next": "^16.3.6",
+ "next": "^16.3.8",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"zod": "^4.6.5",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"babel-plugin-react-compiler": "^1.0.0",
@@ -109,26 +119,26 @@
"typescript": "^6.0.3",
},
},
- "examples/tanstack-start-react/otp-memory": {
- "name": "@repo/example-tanstack-otp",
+ "examples/tanstack-start-react/kitchen-sink": {
+ "name": "@repo/example-tanstack-kitchen-sink",
"dependencies": {
"@repo/shared-react": "workspace:*",
+ "@repo/spike": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
- "authax": "workspace:*",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
"zod": "^4.6.5",
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1",
+ "vite": "^8.3.2",
},
},
"examples/tanstack-start-react/otp-passkey-memory": {
@@ -137,8 +147,8 @@
"@repo/shared-react": "workspace:*",
"@repo/shared-webauthn": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"authax": "workspace:*",
"react": "^19.3.0",
"react-dom": "^19.3.0",
@@ -146,12 +156,12 @@
"zod": "^4.6.5",
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1",
+ "vite": "^8.3.2",
},
},
"examples/tanstack-start-react/otp-passkey-strict-memory": {
@@ -160,8 +170,8 @@
"@repo/shared-react": "workspace:*",
"@repo/shared-webauthn": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"authax": "workspace:*",
"react": "^19.3.0",
"react-dom": "^19.3.0",
@@ -169,58 +179,56 @@
"zod": "^4.6.5",
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1",
+ "vite": "^8.3.2",
},
},
"examples/tanstack-start-react/passkey-memory": {
"name": "@repo/example-tanstack-passkey",
"dependencies": {
"@repo/shared-react": "workspace:*",
- "@repo/shared-webauthn": "workspace:*",
+ "@repo/spike": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
- "authax": "workspace:*",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
"zod": "^4.6.5",
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1",
+ "vite": "^8.3.2",
},
},
"examples/tanstack-start-react/passkey-otp-memory": {
"name": "@repo/example-tanstack-passkey-otp",
"dependencies": {
"@repo/shared-react": "workspace:*",
- "@repo/shared-webauthn": "workspace:*",
+ "@repo/spike": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
- "authax": "workspace:*",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
"zod": "^4.6.5",
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1",
+ "vite": "^8.3.2",
},
},
"packages/authax": {
@@ -228,20 +236,23 @@
"version": "0.0.1",
"devDependencies": {
"@eslint/js": "^10.0.1",
- "@tanstack/react-start": "^1.168.58",
- "eslint": "^10.11.0",
- "neon-testing": "^3.0.1",
- "typescript-eslint": "^8.70.1",
+ "@tanstack/react-start": "^1.168.60",
+ "eslint": "^10.12.0",
+ "neon-testing": "^3.0.2",
+ "typescript-eslint": "^8.71.0",
},
"peerDependencies": {
"@tanstack/react-start": "^1.161.1",
- "next": "^16.3.6",
+ "next": "^16.3.8",
},
"optionalPeers": [
"@tanstack/react-start",
"next",
],
},
+ "packages/spike": {
+ "name": "@repo/spike",
+ },
},
"trustedDependencies": [
"sharp",
@@ -367,57 +378,57 @@
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
- "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="],
+ "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="],
- "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="],
+ "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="],
- "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="],
+ "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="],
- "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="],
+ "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="],
- "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="],
+ "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="],
- "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="],
+ "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="],
- "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="],
+ "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="],
- "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="],
+ "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="],
- "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="],
+ "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="],
- "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="],
+ "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="],
- "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="],
+ "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="],
- "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="],
+ "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="],
- "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="],
+ "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="],
- "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="],
+ "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="],
- "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="],
+ "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="],
- "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="],
+ "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="],
- "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="],
+ "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="],
- "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="],
+ "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="],
- "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="],
+ "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="],
- "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="],
+ "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="],
- "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="],
+ "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="],
- "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="],
+ "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="],
- "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="],
+ "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="],
- "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="],
+ "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="],
- "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="],
+ "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="],
- "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="],
+ "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
@@ -435,23 +446,23 @@
"@neondatabase/api-client": ["@neondatabase/api-client@2.7.3", "", { "dependencies": { "axios": "^1.13.5" } }, "sha512-hqJmcd8qeCNjcsSNlz77peGIwB9SLj8NC6kK6AjPRyb4bUvXB+TCmffx5893/0oRHYjqpua0nBLa5ql8z6acmw=="],
- "@next/env": ["@next/env@16.3.6", "", {}, "sha512-x9Vblze1EbtltQYnNH38xCPWU3TVfBd1eXqA3+w9+BTpedkkdNpAaltXlGQ/nsc1+E0mVTNrtcbX3GoO09zeLQ=="],
+ "@next/env": ["@next/env@16.3.8", "", {}, "sha512-Al9zqHVV7TJv0eFuOU4U7Lvv74PTih4Ch63sk2xCIpSTkE3udFnaOcnzP2lQVymiL7yS9Cj2iClUXlR3EQ5sEw=="],
- "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-E/7GEqaUkt8mk/T8v9lAnrhzR06kdq1ZBkC12F8tAMkdIadwNp3H1KqHynDHrpcTlGCUdq/qu6vUL2aYVyYBdw=="],
+ "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-2JPRMh2nmQG5CiL7cXGL9AGwnPWJQ//cTtAUCT+w511QHk79SYz3LGv/pc5X643B/WEO0rvu3Yww0hqwt3kgeA=="],
- "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-yBE893/nDWTlaiBD1p+qgt7NUen4U5R6FXyH0s67Npq1S3E0cVSef1WIXC2xBRgQvwAvJq6DnS6Y6PrY0cy4Ew=="],
+ "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-GZtCCOBKJ4leVIT/Th0llWKhD1ca92lzbQiS5R5ON9QkoiFnilFsebDae1JU2a3HWoKMEmEZWGs1AGLavVM72Q=="],
- "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-KJDpjBqBPYlvkivmyrp+Qys6k/7ksbqGQvRVc6ZEGfR+cjQxx+nUkJaWmNZJsmoOrqYNbaXByF8wa0lBwDhB3Q=="],
+ "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O659ygeQYqneJ1fBKMpFxIFqYkYswu8IAS1OCKK/4f3ZgJJm1dRz4fVJZRi/kLLWjnBKnebOePA4WNv+sV1pVA=="],
- "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-mqNg2K+hvWskSRb/QM+Ix412DvBsuSF0XV+frTSw5vmoucNnIlynFwKYew8D01bfATErMOM7Bujrf0BA5DRKFA=="],
+ "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-dSjKSyWpzxoO1d3DIZZcP4XJcNaKeLmxQMFOiYl5vuBRMmweIqnAhty8tAmRsvTss779cK1FtYnDMj40e4TQlg=="],
- "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-nFncBNGAYouRHjRVaITs9beZRfhX4ssVwpnvPIAbkZVH6LtGoAVlH4bJ8Cnf9SOo9bsXgPFer/GdHtEE3JNOkw=="],
+ "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-lbqOuz3RPRcv+o9msNsJw5x4+Y1ZwPTs6vmL6DCf7i0fZfvng/F59wyeDwqHIvV0mK//RBy/jJkZ+nCKsSMXjQ=="],
- "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.6", "", { "os": "linux", "cpu": "x64" }, "sha512-5Mf3cHDGR/Iz0ng2Bj3zUR3p5QS9YK3Hn2QiAfavFmyF48zwThAjpFoiTKNIcOHLYS4zEk+gzyJ/9deQ2ZB8yQ=="],
+ "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.8", "", { "os": "linux", "cpu": "x64" }, "sha512-+316WswI8ScVgZeUd+1KGaXkHhaYQzCjvH/05TZSpJ8zBizb1a4G7DtO7F12jcBIqMOtsz9ji1t48fmKtzqsGA=="],
- "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-0jkJy0C2kbrJWTk4YLa3xk80pVBpx8FCHJym7CnUfDAXe/FWv5qT7SQJbR0KuemyxaEDlEx5WT4VQJoTW+/9Qw=="],
+ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-ji0gd4kMYUxO+1fJBIbiBVRCjzG/lloiyCccnlebvb1ZJ5qXCPZqYg4Jl1DrrixnWNMKylzgpmMWx0yNDYXlzw=="],
- "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.6", "", { "os": "win32", "cpu": "x64" }, "sha512-/YXjI1e5OXcZ7YpxRwgP/1jAV/SBKTzeVKqN2mk7mLpcICsyn3Gl5+dIfDTJp70M0ccMhyMMRso4v6mPDCGepg=="],
+ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.8", "", { "os": "win32", "cpu": "x64" }, "sha512-WcTlaKt/TWkh5kUjdJcUmB1XgZ+1c6fz4Y9fDHL73YNSdGaUWjceeWrrlwF0nv19iABYWC4iAq1oX1w4Bn0vfg=="],
"@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="],
@@ -485,17 +496,19 @@
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-+bN6OuVld/9diT/RLSXSW7JE6CvNE3gL9XsAEjULi1nUsXd6DNO6GuA9jNdNb3r8PdJFnYHr5aypNV1Oj3Rd9g=="],
- "@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="],
+ "@oxc-project/types": ["@oxc-project/types@0.152.0", "", {}, "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw=="],
"@repo/example-bun-otp": ["@repo/example-bun-otp@workspace:examples/bun-react/otp-memory-cookie"],
"@repo/example-bun-otp-header": ["@repo/example-bun-otp-header@workspace:examples/bun-react/otp-memory-header"],
+ "@repo/example-bun-spike-otp": ["@repo/example-bun-spike-otp@workspace:examples/bun/otp"],
+
"@repo/example-convex-otp": ["@repo/example-convex-otp@workspace:examples/convex-react/otp"],
"@repo/example-nextjs-otp": ["@repo/example-nextjs-otp@workspace:examples/nextjs/otp-memory"],
- "@repo/example-tanstack-otp": ["@repo/example-tanstack-otp@workspace:examples/tanstack-start-react/otp-memory"],
+ "@repo/example-tanstack-kitchen-sink": ["@repo/example-tanstack-kitchen-sink@workspace:examples/tanstack-start-react/kitchen-sink"],
"@repo/example-tanstack-otp-passkey": ["@repo/example-tanstack-otp-passkey@workspace:examples/tanstack-start-react/otp-passkey-memory"],
@@ -509,35 +522,37 @@
"@repo/shared-webauthn": ["@repo/shared-webauthn@workspace:examples/shared/webauthn"],
- "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.11", "", { "os": "android", "cpu": "arm" }, "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw=="],
+ "@repo/spike": ["@repo/spike@workspace:packages/spike"],
+
+ "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.12", "", { "os": "android", "cpu": "arm" }, "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A=="],
- "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.11", "", { "os": "android", "cpu": "arm64" }, "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA=="],
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.12", "", { "os": "android", "cpu": "arm64" }, "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg=="],
- "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ=="],
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w=="],
- "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w=="],
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA=="],
- "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow=="],
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ=="],
- "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.11", "", { "os": "linux", "cpu": "arm" }, "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ=="],
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.12", "", { "os": "linux", "cpu": "arm" }, "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g=="],
- "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw=="],
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw=="],
- "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg=="],
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw=="],
- "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw=="],
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ=="],
- "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ=="],
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw=="],
- "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A=="],
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw=="],
- "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.11", "", { "os": "linux", "cpu": "x64" }, "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg=="],
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.12", "", { "os": "linux", "cpu": "x64" }, "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g=="],
- "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.11", "", { "os": "none", "cpu": "arm64" }, "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA=="],
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA=="],
- "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw=="],
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew=="],
- "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.11", "", { "os": "win32", "cpu": "x64" }, "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA=="],
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.12", "", { "os": "win32", "cpu": "x64" }, "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
@@ -577,37 +592,37 @@
"@tanstack/history": ["@tanstack/history@1.162.4", "", {}, "sha512-utTS5L2OkeYUzXGohL1Z8sefu1GLNOJcxe8Hd6iIdc/Xo1K1nDB2JEp4iSFhvYh33xKC9V91TxrS8qfrpoKobQ=="],
- "@tanstack/react-router": ["@tanstack/react-router@1.170.39", "", { "dependencies": { "@tanstack/history": "1.162.4", "@tanstack/react-store": "^0.11.0", "@tanstack/router-core": "1.171.32", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-wLZi/hXW/kTeLlMIvI2VB8Oj38GLz8YkHmTM14lLScAzx79UZEwBHWMj6JZw/imuyuhrcmbe8KTFsFBHzJUftg=="],
+ "@tanstack/react-router": ["@tanstack/react-router@1.170.41", "", { "dependencies": { "@tanstack/history": "1.162.4", "@tanstack/react-store": "^0.11.2", "@tanstack/router-core": "1.171.34", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Mpvw8Wm5MGDbTqI8Yc9flLKxbOHwoWocv0H2aGKcgw2a017oK5nZWqa954P+DrUx/8fI98YNywnHP5XcD3tQ9w=="],
- "@tanstack/react-start": ["@tanstack/react-start@1.168.58", "", { "dependencies": { "@tanstack/react-router": "1.170.39", "@tanstack/react-start-client": "1.168.37", "@tanstack/react-start-rsc": "0.1.57", "@tanstack/react-start-server": "1.167.44", "@tanstack/router-utils": "1.162.3", "@tanstack/start-client-core": "1.170.32", "@tanstack/start-plugin-core": "1.171.47", "@tanstack/start-server-core": "1.169.37", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-PYIUFOsGp98QvqbW6GDkRqjAZNYJ1c+rA0pGtEwb6oDQeECnrbLij1NbGLPed/nVnROyBNwvq/+rAK3Hb3j9eg=="],
+ "@tanstack/react-start": ["@tanstack/react-start@1.168.60", "", { "dependencies": { "@tanstack/react-router": "1.170.41", "@tanstack/react-start-client": "1.168.39", "@tanstack/react-start-rsc": "0.1.59", "@tanstack/react-start-server": "1.167.46", "@tanstack/router-utils": "1.162.3", "@tanstack/start-client-core": "1.170.34", "@tanstack/start-plugin-core": "1.171.49", "@tanstack/start-server-core": "1.169.39", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-WMOM0Xlh2lPbVlbjYM6uOxOpiaWRPvODm/dwkLsGKq5tjyHOHZmKckJgwlrZl7OQUqFlOZHYj8bKUTgoeg8cUQ=="],
- "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.37", "", { "dependencies": { "@tanstack/react-router": "1.170.39", "@tanstack/router-core": "1.171.32", "@tanstack/start-client-core": "1.170.32" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-koHPF9H/a94Akn33nSUKPEeRIs/JpZvR3ekYxKnUQshl6N24R5kTr7PVkT+qcPxqD+OAu2sKjdBqIXvKLVQjzQ=="],
+ "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.39", "", { "dependencies": { "@tanstack/react-router": "1.170.41", "@tanstack/router-core": "1.171.34", "@tanstack/start-client-core": "1.170.34" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Uby0uo37JlQgUKHU0snDwH9nYiEeAjm9+7pUOFgycU/UszEILC2SO2DSCoNnCMuHKj9z+4URKzBYIP2exT9+wg=="],
- "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.57", "", { "dependencies": { "@tanstack/react-router": "1.170.39", "@tanstack/router-core": "1.171.32", "@tanstack/router-utils": "1.162.3", "@tanstack/start-client-core": "1.170.32", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.47", "@tanstack/start-storage-context": "1.167.34", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.30", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-NIGJ8gktn6XDqDsUtW7onYWNLAO85ygpKXnfYdQZE3lQIFZLTzeI9Qcm67w1h4vRuxhsUNUSleMyhBqsmF8tLw=="],
+ "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.59", "", { "dependencies": { "@tanstack/react-router": "1.170.41", "@tanstack/router-core": "1.171.34", "@tanstack/router-utils": "1.162.3", "@tanstack/start-client-core": "1.170.34", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.49", "@tanstack/start-storage-context": "1.167.36", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.30", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-M2Lqxkk5C1zQncQs5paqX+/OLdOht51pg6D84YfLMrNsjhU/nzb6wd92MIja0mi5i23+L+fxLLjQYR2DObjBlw=="],
- "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.44", "", { "dependencies": { "@tanstack/react-router": "1.170.39", "@tanstack/router-core": "1.171.32", "@tanstack/start-server-core": "1.169.37" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-eIdyRoq+rTNpCRx9xYJQaGHQmIUpTR80BGSmRy7pk/BbbdfXmyEgB570LDA/vAHySM1gh6PSTKfwGHBJ9FeqtQ=="],
+ "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.46", "", { "dependencies": { "@tanstack/react-router": "1.170.41", "@tanstack/router-core": "1.171.34", "@tanstack/start-server-core": "1.169.39" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-K7z8xBOEdp8u5CZCxWrbS0tFtk5XZGMdhssLeZlRZKlLDbX7BKLdMtJ7th5sR6QVt9MTUAg3LcoqvJMPSFF+lg=="],
- "@tanstack/react-store": ["@tanstack/react-store@0.11.1", "", { "dependencies": { "@tanstack/store": "0.11.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ=="],
+ "@tanstack/react-store": ["@tanstack/react-store@0.11.2", "", { "dependencies": { "@tanstack/store": "0.11.2", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-oj5O5DmiMWE+ih+SioX2ayC1kbK7d3fpLx0B75fPJr9nY3zU+hoW010yr64qXsY/9fyiLVE2bn5mGNV3owm8RQ=="],
- "@tanstack/router-core": ["@tanstack/router-core@1.171.32", "", { "dependencies": { "@tanstack/history": "1.162.4", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-X86Jqk3vB2KJcUfS6oLi7B7yxbaa59QEhZRPPP1fRx3k+Jw/Fu1UYVBcRtdwK/OccQnrlQdVoKvNO3QXmyEBOw=="],
+ "@tanstack/router-core": ["@tanstack/router-core@1.171.34", "", { "dependencies": { "@tanstack/history": "1.162.4", "cookie-es": "^3.0.0", "seroval": "^1.6.7", "seroval-plugins": "^1.6.7" } }, "sha512-X+19pGuuLVPvvAvPYw9v/+ktRk5cCll8AtfpBpOhDRmVgUMrfqze0467xtq8YRVVs/mmo5rVtWn/7Tp32RDBKw=="],
- "@tanstack/router-generator": ["@tanstack/router-generator@1.167.38", "", { "dependencies": { "@babel/types": "^7.29.8", "@tanstack/router-core": "1.171.32", "@tanstack/router-utils": "1.162.3", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.9.6", "zod": "^4.5.4" } }, "sha512-lkqgFleDgfkW+QONVMKBs+ZGDUF0v9R9FkplS/GZvKDFpfnu+tZxmGlV2pryU1TDxyuSyjh56AtIla3NMrlScw=="],
+ "@tanstack/router-generator": ["@tanstack/router-generator@1.167.40", "", { "dependencies": { "@babel/types": "^7.29.8", "@tanstack/router-core": "1.171.34", "@tanstack/router-utils": "1.162.3", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.9.6", "zod": "^4.5.4" } }, "sha512-WkEueqEvsBBudzO0fx1nWfOmRNW8OAvBoVABTGls9bAWzjdC+KDGHvjCNrvn88ofs1F6gqL8aWnzKCbxW3dCyQ=="],
- "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.40", "", { "dependencies": { "@babel/core": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "@tanstack/router-core": "1.171.32", "@tanstack/router-generator": "1.167.38", "@tanstack/router-utils": "1.162.3", "chokidar": "^5.0.0", "unplugin": "^3.3.0", "zod": "^4.5.4" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.38", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-ifiXjjR4uivxwRDhgYAcfyrcu+Mwx+vlG0QjjQ7N5C5sKmzpt5UtsL21TNVvZDSZl2Vae8DcL84Z02fOs8ZZMQ=="],
+ "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.42", "", { "dependencies": { "@babel/core": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "@tanstack/router-core": "1.171.34", "@tanstack/router-generator": "1.167.40", "@tanstack/router-utils": "1.162.3", "chokidar": "^5.0.0", "unplugin": "^3.3.0", "zod": "^4.5.4" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.41", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-vAR5WXJh/ErseEWQftDhsJcr2c++ucRQ6FbqN3ygMO44+ZxxRfW3sIpZmddHROVSmBk8jc6MrwjTPNjVhd62Vw=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.162.3", "", { "dependencies": { "@babel/generator": "^7.29.8", "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "ansis": "^4.3.1", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.4", "pathe": "^2.0.3", "tinyglobby": "^0.2.17" } }, "sha512-Icb0xGuG1+54IV0WMLRcc3ErTx2HeJWyPG661FkQ8UT6guoBq1FJjFRqlG/JS0xi4mFFkBhvwO7tbLa32f3yxA=="],
- "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.32", "", { "dependencies": { "@tanstack/router-core": "1.171.32", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.34", "seroval": "^1.6.2" } }, "sha512-kawgeg3Ej/JvGeEN7txFdVN9kXOST9wl7yPojOGESiomwiubHKRdYUS6sBskW91j44H+HirQUppP4+Ha7kjb4A=="],
+ "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.34", "", { "dependencies": { "@tanstack/router-core": "1.171.34", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.36", "seroval": "^1.6.7" } }, "sha512-LfwZSbau2MLLZ/uPAoO+NC9fK3J66c0wLhnNQqIa+Goqw7bXPjkHwlsjY7evlUz1AnyhhQ1Qab95v+VmP0TZ9A=="],
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="],
- "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.47", "", { "dependencies": { "@babel/code-frame": "7.29.7", "@babel/core": "^7.29.7", "@babel/types": "^7.29.8", "@jridgewell/remapping": "^2.3.5", "@tanstack/router-core": "1.171.32", "@tanstack/router-generator": "1.167.38", "@tanstack/router-plugin": "1.168.40", "@tanstack/router-utils": "1.162.3", "@tanstack/start-server-core": "1.169.37", "exsolve": "^1.1.1", "lightningcss": "^1.33.0", "pathe": "^2.0.3", "picomatch": "^4.0.7", "seroval": "^1.6.2", "source-map": "^0.7.6", "srvx": "^0.11.22", "tinyglobby": "^0.2.17", "ufo": "^1.6.4", "vitefu": "^1.1.3", "xmlbuilder2": "^4.0.3", "zod": "^4.5.4" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-yDduux0silP+81gZM+fY7OVebC8RcU8ud6v9E3+8fzn6wA0RnuFHbVC/UkN86z1pAaXbon3Ps8rqCq6S9mv8TA=="],
+ "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.49", "", { "dependencies": { "@babel/code-frame": "7.29.7", "@babel/core": "^7.29.7", "@babel/types": "^7.29.8", "@jridgewell/remapping": "^2.3.5", "@tanstack/router-core": "1.171.34", "@tanstack/router-generator": "1.167.40", "@tanstack/router-plugin": "1.168.42", "@tanstack/router-utils": "1.162.3", "@tanstack/start-server-core": "1.169.39", "exsolve": "^1.1.1", "lightningcss": "^1.33.0", "pathe": "^2.0.3", "picomatch": "^4.0.7", "seroval": "^1.6.7", "source-map": "^0.7.6", "srvx": "^0.11.22", "tinyglobby": "^0.2.17", "ufo": "^1.6.4", "vitefu": "^1.1.3", "xmlbuilder2": "^4.0.3", "zod": "^4.5.4" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-HPOb7LnMc71k38cd5uvMybPPUU/Q/56KvSqQzSWz7cps1ZDFXaxUM6EFaFHLCdgTWqGexNGDdRE+sUVPVBM4Xg=="],
- "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.37", "", { "dependencies": { "@tanstack/history": "1.162.4", "@tanstack/router-core": "1.171.32", "@tanstack/start-client-core": "1.170.32", "@tanstack/start-storage-context": "1.167.34", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.6.2" } }, "sha512-CjYMXg2XISMEJt9UVR4lbMRsnRyWPrdyuF8iuJ4gxTc1sVNRciwMVpMPVGQG2KuIma3ewlNxw8+Tpx2tnS3zRA=="],
+ "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.39", "", { "dependencies": { "@tanstack/history": "1.162.4", "@tanstack/router-core": "1.171.34", "@tanstack/start-client-core": "1.170.34", "@tanstack/start-storage-context": "1.167.36", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.6.7" } }, "sha512-pqqkxhXY46nuFSYt6TsMGyABiplUzHFHndKAlRRGC0fWDrJCH1koWxVMoxySa51ZETV79f/z73b5UZ1jfIkXwQ=="],
- "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.34", "", { "dependencies": { "@tanstack/router-core": "1.171.32" } }, "sha512-cRPE0kjt1CnOIhepmNOvRFwCGhDAnCRohPD3HmzqJJThPkixLBRuLDvIYp4hRPtM1TJ9hiKbYl24IxAhjZO5TA=="],
+ "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.36", "", { "dependencies": { "@tanstack/router-core": "1.171.34" } }, "sha512-x+xCXK2GtFzY6NbXqAg8YpU6cwAjDfMeYSNzxfGzntpjbwBRHHrXeC/kYUnBBQCV7wK4kJ8vBBQGP0OyYTasUg=="],
- "@tanstack/store": ["@tanstack/store@0.11.1", "", {}, "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA=="],
+ "@tanstack/store": ["@tanstack/store@0.11.2", "", {}, "sha512-sJ4mjol8uQsHV0gOJzzjwXfh2Fwm+Sz0+8deqiTm4jGbMdjzNSW+xZCFm0kUa870uhd8yi+DpKnZb5Kc4apa0Q=="],
"@tanstack/virtual-file-routes": ["@tanstack/virtual-file-routes@1.162.0", "", {}, "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA=="],
@@ -623,37 +638,37 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
- "@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="],
+ "@types/node": ["@types/node@26.6.4", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg=="],
"@types/react": ["@types/react@19.3.0", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg=="],
"@types/react-dom": ["@types/react-dom@19.3.0", "", { "peerDependencies": { "@types/react": "^19.3.0" } }, "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q=="],
- "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.70.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.70.1", "@typescript-eslint/type-utils": "8.70.1", "@typescript-eslint/utils": "8.70.1", "@typescript-eslint/visitor-keys": "8.70.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.70.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nDNrUQ/4ruSNYbu749TRY7cfrzPtoLHEXSNBI8aaNY32LlZCajixqRf3FqcKC4p5Cam4VOHYx/t+i5+nKXvrqA=="],
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.71.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.71.0", "@typescript-eslint/type-utils": "8.71.0", "@typescript-eslint/utils": "8.71.0", "@typescript-eslint/visitor-keys": "8.71.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.71.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw=="],
- "@typescript-eslint/parser": ["@typescript-eslint/parser@8.70.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.70.1", "@typescript-eslint/types": "8.70.1", "@typescript-eslint/typescript-estree": "8.70.1", "@typescript-eslint/visitor-keys": "8.70.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nO974WLllwhSFWQXnMLj6nDGa8f0khKEz1JzpPJ1u7Vm/4X1X6ZHajpoknU4bb41vJyMB0HHVyS2GqdhWfIXZw=="],
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.71.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.71.0", "@typescript-eslint/types": "8.71.0", "@typescript-eslint/typescript-estree": "8.71.0", "@typescript-eslint/visitor-keys": "8.71.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A=="],
- "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.70.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.70.1", "@typescript-eslint/types": "^8.70.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-62xOgboPfwc3/IgPSX/W6oQR3ZbF04194FPGUGH8HL8iLFHbt/456/8Ph1wLNUgVF+s94FlHoipBsz+v7+LMnA=="],
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.71.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.71.0", "@typescript-eslint/types": "^8.71.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ=="],
- "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.70.1", "", { "dependencies": { "@typescript-eslint/types": "8.70.1", "@typescript-eslint/visitor-keys": "8.70.1" } }, "sha512-Pa0EeSeAusQc1WbjQMac+YfenewYTBu0KjgYvkUKwhXaHUKbFog23Dm/rp0DX/6tyYOQ3Xl1a+3EcFNZynGHCw=="],
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.71.0", "", { "dependencies": { "@typescript-eslint/types": "8.71.0", "@typescript-eslint/visitor-keys": "8.71.0" } }, "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw=="],
- "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.70.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-jumze1fPI+sDOaM2TWGQdn39PDxTr7TZGeuyLkAbNyx2vtMT3uRnVKChN0hfht5V2TugphJzF6bYXvBcE09qqg=="],
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.71.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg=="],
- "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.70.1", "", { "dependencies": { "@typescript-eslint/types": "8.70.1", "@typescript-eslint/typescript-estree": "8.70.1", "@typescript-eslint/utils": "8.70.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7zKTnyvaVWqzLZHPFQtX1hVHqgkMC+WebPWakNCSyrQVbIP1AM0L0TlBZtACldIRb6PptI8Odk+jyZ5kP3B1VA=="],
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.71.0", "", { "dependencies": { "@typescript-eslint/types": "8.71.0", "@typescript-eslint/typescript-estree": "8.71.0", "@typescript-eslint/utils": "8.71.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw=="],
- "@typescript-eslint/types": ["@typescript-eslint/types@8.70.1", "", {}, "sha512-Dm1ypdhhrGCTyyehxElhgJ6kgk8MVCv5qXdoOVqPr1uqk42jX8KjrZqhROvdShczA8qrDoYiOWn1ykWlx2k81Q=="],
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.71.0", "", {}, "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ=="],
- "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.70.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.70.1", "@typescript-eslint/tsconfig-utils": "8.70.1", "@typescript-eslint/types": "8.70.1", "@typescript-eslint/visitor-keys": "8.70.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TU8PwyGN0PQJUcE96mw8eCQ44SmxGdQlJmlWakHaHQ15eIuuvye5yNtmh/i6oS88jzXVQB71xdNkbkB/fMwL0g=="],
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.71.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.71.0", "@typescript-eslint/tsconfig-utils": "8.71.0", "@typescript-eslint/types": "8.71.0", "@typescript-eslint/visitor-keys": "8.71.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw=="],
- "@typescript-eslint/utils": ["@typescript-eslint/utils@8.70.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.70.1", "@typescript-eslint/types": "8.70.1", "@typescript-eslint/typescript-estree": "8.70.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Esgul8MsnKnRLdYU2Eb2cRV9bS5HJYtKj1ByJnOzzG2M58DGdSUQ1jUuILxipqcpB2h9WLrbD5GijIWUjX/Tqw=="],
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.71.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.71.0", "@typescript-eslint/types": "8.71.0", "@typescript-eslint/typescript-estree": "8.71.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ=="],
- "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.70.1", "", { "dependencies": { "@typescript-eslint/types": "8.70.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-Vwj9lUIW5Xq3wQ9w6gv3R86g1hMK8f2zNOdGTAgeXUMMXFK78G9ruCjjqutHMNJc0+CH7LYRnHeUB9IT8wFmcw=="],
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.71.0", "", { "dependencies": { "@typescript-eslint/types": "8.71.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="],
- "@vitest/mocker": ["@vitest/mocker@5.0.2", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.2", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA=="],
+ "@vitest/mocker": ["@vitest/mocker@5.0.3", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.31", "@vitest/spy": "5.0.3", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg=="],
- "@vitest/spy": ["@vitest/spy@5.0.2", "", {}, "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg=="],
+ "@vitest/spy": ["@vitest/spy@5.0.3", "", {}, "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg=="],
"acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="],
@@ -681,11 +696,11 @@
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
- "baseline-browser-mapping": ["baseline-browser-mapping@2.11.26", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-GLQdD3y6UF8iVuMJl5fHgE4jdn/ua7n+toKfLgNlg3BqQtOZjpy68T8Tup8/wGWZCDlm7KMg7tPb4MPn7oN0TQ=="],
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.11.27", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ=="],
"brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="],
- "browserslist": ["browserslist@4.29.1", "", { "dependencies": { "baseline-browser-mapping": "^2.11.25", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.438", "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-AUdjuRyCNGUYtqpqfTmWyM4fXay8yIQhmLnvYe/THMGfT9B/34X7xQd3ifKxwyNPPpowVBjLb+64BN9Rn1mizw=="],
+ "browserslist": ["browserslist@4.29.3", "", { "dependencies": { "baseline-browser-mapping": "^2.11.26", "caniuse-lite": "^1.0.30001813", "electron-to-chromium": "^1.5.439", "node-releases": "^2.0.57", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA=="],
"bun": ["bun@1.4.2", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.4.2", "@oven/bun-darwin-x64": "1.4.2", "@oven/bun-freebsd-aarch64": "1.4.2", "@oven/bun-freebsd-x64": "1.4.2", "@oven/bun-linux-aarch64": "1.4.2", "@oven/bun-linux-aarch64-android": "1.4.2", "@oven/bun-linux-aarch64-musl": "1.4.2", "@oven/bun-linux-x64": "1.4.2", "@oven/bun-linux-x64-android": "1.4.2", "@oven/bun-linux-x64-musl": "1.4.2", "@oven/bun-windows-aarch64": "1.4.2", "@oven/bun-windows-x64": "1.4.2" }, "os": [ "!aix", "!sunos", "!openbsd", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-TrSXo6HJfIEaczpb3kjX82I2pL47vK1QUNmHRCUdz9IzaOwa9lzOXSWwu2l18YHE3sNfGRapVLd4nNm+22vVVA=="],
@@ -697,9 +712,9 @@
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001812", "", {}, "sha512-qN+QNNBr93TCmFrmte0bBCjSDMuRvt78VlHT99qIGPszm4QsqCX8lnyWUFkHi8B7ZBAPq8SH+UqyXNy/odMdng=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001814", "", {}, "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA=="],
- "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+ "chai": ["chai@6.3.0", "", {}, "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
@@ -729,9 +744,9 @@
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
- "electron-to-chromium": ["electron-to-chromium@1.5.439", "", {}, "sha512-qu6QIPXhsb+CRcAiTMNjR4A1y/7tCYKkKjr5CZXRVih6qkDf79peZ2BpEU3qoDBNCRrcy3Mra3X9nG5oruuA7Q=="],
+ "electron-to-chromium": ["electron-to-chromium@1.5.444", "", {}, "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ=="],
- "enhanced-resolve": ["enhanced-resolve@5.25.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-nGXts5znJzmWPu+mIE9izCOzdg63oJca2mDzGWWTth7sr4aCToKcoyFVBQwN75Ij5Pf6p510EwkTqViTRzDV+w=="],
+ "enhanced-resolve": ["enhanced-resolve@5.26.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -749,7 +764,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
- "eslint": ["eslint@10.11.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.3", "@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": "11.1.5 || >11.1.6 <12", "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-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ=="],
+ "eslint": ["eslint@10.12.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.3", "@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": "11.1.5 || >11.1.6 <12", "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-npHjrHb2o4ehH3mE+YuxABR+0gyb0aVWCIolgJDArwCiHEIsonBm3ZKjel5b0U5+a+MbEvIyRCGCzD3eKKK2NA=="],
"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=="],
@@ -769,7 +784,7 @@
"expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="],
- "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="],
+ "exsolve": ["exsolve@1.1.3", "", {}, "sha512-kuzL9zherLQoqDeQaS55AiiIg1kb4KYXaSusTtgZTPlvKP2gGCI82LNmBNh5RjDVUXIcmgx+/Mx7vPwRzBbhJA=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
@@ -789,7 +804,7 @@
"flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="],
- "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="],
+ "follow-redirects": ["follow-redirects@1.16.1", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg=="],
"form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="],
@@ -897,9 +912,9 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
- "neon-testing": ["neon-testing@3.0.1", "", { "dependencies": { "@neondatabase/api-client": "^2.7.3" }, "peerDependencies": { "@neondatabase/serverless": "^1", "vitest": "^3 || ^4" }, "optionalPeers": ["@neondatabase/serverless", "vitest"] }, "sha512-ZYFO6pWwBwVkaWCGH64f5MMnPRS84sJnqT6IUyvRj1VwVElDAZxMNpVhefhnSi91tlZYez0HFSg9uGu1Q3xIHg=="],
+ "neon-testing": ["neon-testing@3.0.2", "", { "dependencies": { "@neondatabase/api-client": "^2.7.3" }, "peerDependencies": { "@neondatabase/serverless": "^1", "vitest": "^3 || ^4" }, "optionalPeers": ["@neondatabase/serverless", "vitest"] }, "sha512-KQi2wyAZTB0QeiWdAjspsJjTuzpI0UodRy6XzCXQDa2c/5fZ4tWVj5JnnefZTHAoBMKwC9nyGP3YABN6gyP8/g=="],
- "next": ["next@16.3.6", "", { "dependencies": { "@next/env": "16.3.6", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.6", "@next/swc-darwin-x64": "16.3.6", "@next/swc-linux-arm64-gnu": "16.3.6", "@next/swc-linux-arm64-musl": "16.3.6", "@next/swc-linux-x64-gnu": "16.3.6", "@next/swc-linux-x64-musl": "16.3.6", "@next/swc-win32-arm64-msvc": "16.3.6", "@next/swc-win32-x64-msvc": "16.3.6", "sharp": "^0.35.4" }, "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-L+otWM/aQbYTx98aZhgEoMb4bZAXx1YVW4UMA/vuCyCoWG5HJyZUili8QAkqzrcC+5///tsz3s0M+SlyB5bLMw=="],
+ "next": ["next@16.3.8", "", { "dependencies": { "@next/env": "16.3.8", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.8", "@next/swc-darwin-x64": "16.3.8", "@next/swc-linux-arm64-gnu": "16.3.8", "@next/swc-linux-arm64-musl": "16.3.8", "@next/swc-linux-x64-gnu": "16.3.8", "@next/swc-linux-x64-musl": "16.3.8", "@next/swc-win32-arm64-msvc": "16.3.8", "@next/swc-win32-x64-msvc": "16.3.8", "sharp": "^0.35.4" }, "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-U7QEZaTini6wKrb8A8hqLLqYQyCetegKjCpJOyxk642vWoMoU1x5PyZCJFvgYgiptA8xc5j/9xYlZFO7w9Sjmw=="],
"node-releases": ["node-releases@2.0.57", "", {}, "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw=="],
@@ -941,7 +956,7 @@
"readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="],
- "rolldown": ["rolldown@1.2.11", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.11", "@rolldown/binding-android-arm64": "1.2.11", "@rolldown/binding-darwin-arm64": "1.2.11", "@rolldown/binding-darwin-x64": "1.2.11", "@rolldown/binding-freebsd-x64": "1.2.11", "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", "@rolldown/binding-linux-arm64-gnu": "1.2.11", "@rolldown/binding-linux-arm64-musl": "1.2.11", "@rolldown/binding-linux-ppc64-gnu": "1.2.11", "@rolldown/binding-linux-s390x-gnu": "1.2.11", "@rolldown/binding-linux-x64-gnu": "1.2.11", "@rolldown/binding-linux-x64-musl": "1.2.11", "@rolldown/binding-openharmony-arm64": "1.2.11", "@rolldown/binding-win32-arm64-msvc": "1.2.11", "@rolldown/binding-win32-x64-msvc": "1.2.11" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw=="],
+ "rolldown": ["rolldown@1.2.12", "", { "dependencies": { "@oxc-project/types": "=0.152.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.12", "@rolldown/binding-android-arm64": "1.2.12", "@rolldown/binding-darwin-arm64": "1.2.12", "@rolldown/binding-darwin-x64": "1.2.12", "@rolldown/binding-freebsd-x64": "1.2.12", "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", "@rolldown/binding-linux-arm64-gnu": "1.2.12", "@rolldown/binding-linux-arm64-musl": "1.2.12", "@rolldown/binding-linux-ppc64-gnu": "1.2.12", "@rolldown/binding-linux-s390x-gnu": "1.2.12", "@rolldown/binding-linux-x64-gnu": "1.2.12", "@rolldown/binding-linux-x64-musl": "1.2.12", "@rolldown/binding-openharmony-arm64": "1.2.12", "@rolldown/binding-win32-arm64-msvc": "1.2.12", "@rolldown/binding-win32-x64-msvc": "1.2.12" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ=="],
"rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="],
@@ -949,11 +964,11 @@
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
- "seroval": ["seroval@1.6.7", "", {}, "sha512-AeDcLh0yO2SFm9W71essgnSzLV9DI8ZH0x0knXn2DMnUZj728mpLbxjlbB6IqKCmqh8JA3cEqRyGoNkt584JcQ=="],
+ "seroval": ["seroval@1.6.8", "", {}, "sha512-HlSgSAkTk4EqHcje1ptJjfZi1YDv5KbhVJ/d3P7T/nAXua2VmDu+AKDX5VTdFfZf48nDkWB2TKYt0DrCSa+3wg=="],
- "seroval-plugins": ["seroval-plugins@1.6.7", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4Nk35ttD3DTDJW4hgw5StsVAPeU6qnDFnULAouw6tQ7oLTV/ICXrWpsXo2EE52eSP2joUMazbVf52mFEcADqRw=="],
+ "seroval-plugins": ["seroval-plugins@1.6.8", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-N7mWAMydj89EnYTHtRpS3LBrAz3J9O6oVrqpuXuvlAHfAUG8WEKqtbAd7SBiJNY5NfCAFhSdyAh7wI4Shjqy4g=="],
- "sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="],
+ "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -961,11 +976,11 @@
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
- "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+ "source-map-js": ["source-map-js@1.2.2", "", {}, "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw=="],
"srvx": ["srvx@0.11.22", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ=="],
- "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
+ "std-env": ["std-env@4.3.0", "", {}, "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
@@ -973,9 +988,9 @@
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
- "tinybench": ["tinybench@6.1.4", "", {}, "sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ=="],
+ "tinybench": ["tinybench@6.2.0", "", {}, "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q=="],
- "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="],
+ "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
@@ -987,7 +1002,7 @@
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
- "typescript-eslint": ["typescript-eslint@8.70.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.70.1", "@typescript-eslint/parser": "8.70.1", "@typescript-eslint/typescript-estree": "8.70.1", "@typescript-eslint/utils": "8.70.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AcWG7KDjZ2THNXsgwttMaGmzVi0VFRlFYfqFHYQRbDpF3owuYbuiL8c7UUrd2k8s3PoSfIQrWfrGXfcElrWLYA=="],
+ "typescript-eslint": ["typescript-eslint@8.71.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.71.0", "@typescript-eslint/parser": "8.71.0", "@typescript-eslint/typescript-estree": "8.71.0", "@typescript-eslint/utils": "8.71.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw=="],
"ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="],
@@ -1001,17 +1016,17 @@
"use-sync-external-store": ["use-sync-external-store@1.7.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A=="],
- "vite": ["vite@8.3.1", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.9", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA=="],
+ "vite": ["vite@8.3.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.11", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w=="],
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
- "vitest": ["vitest@5.0.2", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.2", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "^6.1.4", "tinyexec": "^1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "^3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.2", "@vitest/browser-preview": "5.0.2", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.2", "@vitest/coverage-v8": "5.0.2", "@vitest/ui": "5.0.2", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg=="],
+ "vitest": ["vitest@5.0.3", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/mocker": "5.0.3", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", "magic-string": "^1.2.3", "obug": "^2.1.4", "picomatch": "^4.0.7", "std-env": "^4.2.0", "tinybench": "^6.1.4", "tinyexec": "^1.3.0", "tinyglobby": "^0.2.17", "why-is-node-running": "3.2.1" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "5.0.3", "@vitest/browser-preview": "5.0.3", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", "@vitest/coverage-istanbul": "5.0.3", "@vitest/coverage-v8": "5.0.3", "@vitest/ui": "5.0.3", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
- "why-is-node-running": ["why-is-node-running@3.2.2", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A=="],
+ "why-is-node-running": ["why-is-node-running@3.2.1", "", { "bin": { "why-is-node-running": "cli.js" } }, "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
@@ -1049,7 +1064,7 @@
"@tanstack/router-generator/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
- "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.10", "", {}, "sha512-HpbUakT7xp5miBUywCHf36ZEuAJNklBJDDsGpUIjMzOSmM8ELSfA9Sa/QDPeNeqeoN31u+UTCkL4klCOVvRm4Q=="],
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.12", "", {}, "sha512-/8UvqAPU9DGTI9k4mxtf49U37Isfwr8Uts96+SBHIkFxPJnHS0Ew4f00sM4Scd8V8EjM0jUNtVDdv1kPdt35lg=="],
"next/postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
diff --git a/examples/bun-react/otp-memory-cookie/src/index.css b/examples/bun-react/otp-memory-cookie/src/index.css
index 017b778..3306d99 100644
--- a/examples/bun-react/otp-memory-cookie/src/index.css
+++ b/examples/bun-react/otp-memory-cookie/src/index.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/bun-react/otp-memory-header/src/index.css b/examples/bun-react/otp-memory-header/src/index.css
index 017b778..3306d99 100644
--- a/examples/bun-react/otp-memory-header/src/index.css
+++ b/examples/bun-react/otp-memory-header/src/index.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/tanstack-start-react/otp-memory/LICENSE.md b/examples/bun/otp/LICENSE.md
similarity index 100%
rename from examples/tanstack-start-react/otp-memory/LICENSE.md
rename to examples/bun/otp/LICENSE.md
diff --git a/examples/tanstack-start-react/otp-memory/README.md b/examples/bun/otp/README.md
similarity index 80%
rename from examples/tanstack-start-react/otp-memory/README.md
rename to examples/bun/otp/README.md
index 48385aa..1a1ab65 100644
--- a/examples/tanstack-start-react/otp-memory/README.md
+++ b/examples/bun/otp/README.md
@@ -1,4 +1,4 @@
-# otp-memory
+# otp
## License
diff --git a/examples/bun/otp/package.json b/examples/bun/otp/package.json
new file mode 100644
index 0000000..a05b7cc
--- /dev/null
+++ b/examples/bun/otp/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@repo/example-bun-spike-otp",
+ "private": true,
+ "license": "MIT-0",
+ "type": "module",
+ "scripts": {
+ "dev": "bun --hot src/server.ts",
+ "typecheck": "tsc"
+ },
+ "dependencies": {
+ "@repo/spike": "workspace:*"
+ },
+ "devDependencies": {
+ "@types/bun": "^1.4.2",
+ "typescript": "^6.0.3"
+ }
+}
diff --git a/examples/bun/otp/src/auth.ts b/examples/bun/otp/src/auth.ts
new file mode 100644
index 0000000..90c794e
--- /dev/null
+++ b/examples/bun/otp/src/auth.ts
@@ -0,0 +1,38 @@
+import { makeOpaqueSessionManager, makeOTP } from "@repo/spike";
+import { db } from "./db";
+
+/** How long someone stays signed in, in seconds */
+export const sessionTtl = 30 * 24 * 60 * 60;
+
+export const sessionManager = makeOpaqueSessionManager<{ userId: string }>({
+ store: async (token, row) => {
+ await db.sessions.insert({
+ token,
+ userId: row.userId,
+ expiresAt: row.expiresAt,
+ });
+ },
+ get: async (token) => {
+ const row = await db.sessions.get(token);
+
+ return row ? { userId: row.userId, expiresAt: row.expiresAt } : null;
+ },
+ delete: async (token) => {
+ await db.sessions.delete(token);
+ },
+ ttl: sessionTtl,
+});
+
+/** Signs someone in or up */
+export const signInOtp = makeOTP({
+ purpose: "sign-in",
+ store: async (row) => {
+ await db.otps.insert(row);
+ },
+ take: async (ticket) => db.otps.delete(ticket),
+ send: async ({ to, otp }) => {
+ console.log(`[OTP] Sign in, ${to}: ${otp}`);
+ },
+ ttl: 10 * 60,
+ attempts: 3,
+});
diff --git a/examples/bun/otp/src/db.ts b/examples/bun/otp/src/db.ts
new file mode 100644
index 0000000..252dc1e
--- /dev/null
+++ b/examples/bun/otp/src/db.ts
@@ -0,0 +1,26 @@
+/**
+ * In-memory database
+ *
+ * Simple in-memory stores for demonstration purposes. In a real app these
+ * would be replaced with database queries.
+ */
+import { makeMemoryTable } from "@repo/spike/demo";
+
+export const db = {
+ users: makeMemoryTable<{
+ userId: string;
+ email: string;
+ }>("userId"),
+ sessions: makeMemoryTable<{
+ token: string;
+ userId: string;
+ expiresAt: number;
+ }>("token"),
+ otps: makeMemoryTable<{
+ ticket: string;
+ identifier: string;
+ otp: string;
+ expiresAt: number;
+ attemptsLeft: number;
+ }>("ticket"),
+};
diff --git a/examples/bun/otp/src/index.html b/examples/bun/otp/src/index.html
new file mode 100644
index 0000000..9d263b3
--- /dev/null
+++ b/examples/bun/otp/src/index.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ AuthAX - Bun OTP example
+
+
+
+
+ AuthAX
+
+
+
+
+
+
+
diff --git a/examples/bun/otp/src/page.ts b/examples/bun/otp/src/page.ts
new file mode 100644
index 0000000..5e0df60
--- /dev/null
+++ b/examples/bun/otp/src/page.ts
@@ -0,0 +1,62 @@
+const viewerText = document.getElementById("viewer") as HTMLParagraphElement;
+const emailForm = document.getElementById("email-form") as HTMLFormElement;
+const otpForm = document.getElementById("otp-form") as HTMLFormElement;
+const errorText = document.getElementById("error") as HTMLParagraphElement;
+
+// Held between sending the OTP and verifying it
+let ticket = "";
+
+async function showViewer() {
+ const response = await fetch("/api/viewer");
+ const viewer: { email: string } | null = await response.json();
+
+ if (viewer) {
+ viewerText.textContent = `Signed in as ${viewer.email}`;
+ viewerText.hidden = false;
+ emailForm.hidden = true;
+ otpForm.hidden = true;
+ } else {
+ emailForm.hidden = false;
+ }
+}
+
+emailForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ const response = await fetch("/api/otp/send", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ email: new FormData(emailForm).get("email") }),
+ });
+
+ if (!response.ok) {
+ errorText.textContent = "Could not send a one-time password";
+ return;
+ }
+
+ const body: { ticket: string } = await response.json();
+ ticket = body.ticket;
+ errorText.textContent = "";
+ emailForm.hidden = true;
+ otpForm.hidden = false;
+});
+
+otpForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+
+ const response = await fetch("/api/otp/verify", {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ ticket, otp: new FormData(otpForm).get("otp") }),
+ });
+
+ if (!response.ok) {
+ errorText.textContent = "Wrong or expired one-time password";
+ return;
+ }
+
+ errorText.textContent = "";
+ await showViewer();
+});
+
+await showViewer();
diff --git a/examples/bun/otp/src/server.ts b/examples/bun/otp/src/server.ts
new file mode 100644
index 0000000..dbfc3eb
--- /dev/null
+++ b/examples/bun/otp/src/server.ts
@@ -0,0 +1,108 @@
+import { db } from "./db";
+import { sessionManager, sessionTtl, signInOtp } from "./auth";
+import page from "./index.html";
+
+/**
+ * Every origin the page is served from. Not read from the request, since
+ * behind a proxy the request's URL holds the internal address.
+ */
+const origins = ["http://localhost:3108"];
+
+const server = Bun.serve({
+ port: 3108,
+ development: process.env.NODE_ENV !== "production",
+ routes: {
+ "/": page,
+
+ /** Sends an OTP to the email address and answers the ticket */
+ "/api/otp/send": {
+ POST: async (req) => {
+ // A page on another site can post here too
+ const origin = req.headers.get("origin");
+
+ if (origin === null || !origins.includes(origin)) {
+ return new Response(null, { status: 403 });
+ }
+
+ const body: unknown = await req.json();
+
+ if (
+ typeof body !== "object" ||
+ body === null ||
+ !("email" in body) ||
+ typeof body.email !== "string"
+ ) {
+ return new Response(null, { status: 400 });
+ }
+
+ const ticket = await signInOtp.send(body.email);
+
+ return Response.json({ ticket });
+ },
+ },
+
+ /** Checks the OTP, finds or creates the user, and signs them in */
+ "/api/otp/verify": {
+ POST: async (req) => {
+ // A page on another site could sign the visitor in to its own account
+ const origin = req.headers.get("origin");
+
+ if (origin === null || !origins.includes(origin)) {
+ return new Response(null, { status: 403 });
+ }
+
+ const body: unknown = await req.json();
+
+ if (
+ typeof body !== "object" ||
+ body === null ||
+ !("ticket" in body) ||
+ !("otp" in body) ||
+ typeof body.ticket !== "string" ||
+ typeof body.otp !== "string"
+ ) {
+ return new Response(null, { status: 400 });
+ }
+
+ const verified = await signInOtp.verify({
+ ticket: body.ticket,
+ otp: body.otp,
+ });
+
+ if (!verified.success) {
+ return Response.json({ error: verified.error }, { status: 400 });
+ }
+
+ const { row: user } = await db.users.findOrInsert(
+ { email: verified.data.identifier },
+ { userId: crypto.randomUUID(), email: verified.data.identifier },
+ );
+
+ const token = await sessionManager.make({ userId: user.userId });
+
+ req.cookies.set("session", token, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === "production",
+ sameSite: "lax",
+ path: "/",
+ maxAge: sessionTtl,
+ });
+
+ return Response.json({ email: user.email });
+ },
+ },
+
+ /** Who is signed in, null when nobody is */
+ "/api/viewer": {
+ GET: async (req) => {
+ const token = req.cookies.get("session");
+ const session = token ? await sessionManager.get(token) : null;
+ const user = session ? await db.users.get(session.userId) : null;
+
+ return Response.json(user ? { email: user.email } : null);
+ },
+ },
+ },
+});
+
+console.log(`Listening on ${server.url}`);
diff --git a/examples/bun/otp/tsconfig.json b/examples/bun/otp/tsconfig.json
new file mode 100644
index 0000000..7d48ea0
--- /dev/null
+++ b/examples/bun/otp/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "Preserve",
+ "moduleResolution": "bundler",
+ "lib": ["ESNext", "DOM"],
+ "types": ["bun"],
+
+ "moduleDetection": "force",
+ "verbatimModuleSyntax": true,
+ "erasableSyntaxOnly": true,
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+ "noEmit": true,
+ "skipLibCheck": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noImplicitOverride": true,
+ "noFallthroughCasesInSwitch": true
+ }
+}
diff --git a/examples/convex-react/otp/package.json b/examples/convex-react/otp/package.json
index 7743124..87251c0 100644
--- a/examples/convex-react/otp/package.json
+++ b/examples/convex-react/otp/package.json
@@ -25,6 +25,6 @@
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1"
+ "vite": "^8.3.2"
}
}
diff --git a/examples/convex-react/otp/src/index.css b/examples/convex-react/otp/src/index.css
index 017b778..3306d99 100644
--- a/examples/convex-react/otp/src/index.css
+++ b/examples/convex-react/otp/src/index.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/nextjs/otp-memory/app/globals.css b/examples/nextjs/otp-memory/app/globals.css
index 017b778..3306d99 100644
--- a/examples/nextjs/otp-memory/app/globals.css
+++ b/examples/nextjs/otp-memory/app/globals.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/nextjs/otp-memory/package.json b/examples/nextjs/otp-memory/package.json
index 6c53648..593fe8b 100644
--- a/examples/nextjs/otp-memory/package.json
+++ b/examples/nextjs/otp-memory/package.json
@@ -12,14 +12,14 @@
"dependencies": {
"authax": "workspace:*",
"@repo/shared-react": "workspace:*",
- "next": "^16.3.6",
+ "next": "^16.3.8",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"zod": "^4.6.5"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"babel-plugin-react-compiler": "^1.0.0",
diff --git a/examples/shared/react/src/styles.css b/examples/shared/react/src/styles.css
index 32b0d79..a084e3a 100644
--- a/examples/shared/react/src/styles.css
+++ b/examples/shared/react/src/styles.css
@@ -1 +1,8 @@
@source ".";
+
+@layer base {
+ button:not(:disabled),
+ [role="button"]:not(:disabled) {
+ cursor: pointer;
+ }
+}
diff --git a/examples/tanstack-start-react/otp-memory/.gitignore b/examples/tanstack-start-react/kitchen-sink/.gitignore
similarity index 100%
rename from examples/tanstack-start-react/otp-memory/.gitignore
rename to examples/tanstack-start-react/kitchen-sink/.gitignore
diff --git a/examples/tanstack-start-react/kitchen-sink/LICENSE.md b/examples/tanstack-start-react/kitchen-sink/LICENSE.md
new file mode 100644
index 0000000..f39badb
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/LICENSE.md
@@ -0,0 +1,18 @@
+MIT No Attribution
+
+Copyright (c) 2025-present Mikael Lirbank and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/tanstack-start-react/kitchen-sink/README.md b/examples/tanstack-start-react/kitchen-sink/README.md
new file mode 100644
index 0000000..ae05f4c
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/README.md
@@ -0,0 +1,5 @@
+# kitchen-sink
+
+## License
+
+MIT-0, see [LICENSE.md](./LICENSE.md).
diff --git a/examples/tanstack-start-react/otp-memory/package.json b/examples/tanstack-start-react/kitchen-sink/package.json
similarity index 71%
rename from examples/tanstack-start-react/otp-memory/package.json
rename to examples/tanstack-start-react/kitchen-sink/package.json
index 395c7f6..1cdbf2a 100644
--- a/examples/tanstack-start-react/otp-memory/package.json
+++ b/examples/tanstack-start-react/kitchen-sink/package.json
@@ -1,5 +1,5 @@
{
- "name": "@repo/example-tanstack-otp",
+ "name": "@repo/example-tanstack-kitchen-sink",
"private": true,
"license": "MIT-0",
"type": "module",
@@ -10,22 +10,22 @@
"typecheck": "tsc"
},
"dependencies": {
- "authax": "workspace:*",
"@repo/shared-react": "workspace:*",
+ "@repo/spike": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
"zod": "^4.6.5"
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1"
+ "vite": "^8.3.2"
}
}
diff --git a/examples/tanstack-start-react/kitchen-sink/src/auth-rpc.ts b/examples/tanstack-start-react/kitchen-sink/src/auth-rpc.ts
new file mode 100644
index 0000000..034920c
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/src/auth-rpc.ts
@@ -0,0 +1,122 @@
+import { createServerFn } from "@tanstack/react-start";
+import { z } from "zod";
+import { db } from "./db";
+import { confirmOtp, sessionManager } from "./auth";
+import { sessionCookie } from "./session-cookie";
+
+/** The session behind the request's cookie, null when there is none */
+async function getIdentity() {
+ const token = sessionCookie.get();
+
+ return token === null ? null : sessionManager.get(token);
+}
+
+/**
+ * Request OTP schema
+ */
+export const requestOtpSchema = z.object({
+ identifier: z.email(),
+});
+
+/**
+ * Verify OTP schema
+ */
+export const verifyOtpSchema = z.object({
+ ticket: z.string(),
+ otp: z.string().length(6),
+});
+
+/**
+ * Send OTP to a new email server function
+ *
+ * Requires an active session. Returns the ticket the client sends back with
+ * the OTP.
+ */
+export const requestChangeEmail = createServerFn({ method: "POST" })
+ .validator(requestOtpSchema)
+ .handler(async ({ data }) => {
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
+
+ return {
+ success: true as const,
+ ticket: await confirmOtp.send(data.identifier),
+ };
+ });
+
+/**
+ * Change email
+ *
+ * Verifies OTP for the new email, then swaps it on the authenticated user.
+ * Requires an active session — the OTP proves ownership of the new address.
+ */
+export const changeEmail = createServerFn({ method: "POST" })
+ .validator(verifyOtpSchema)
+ .handler(async ({ data }) => {
+ const identity = await getIdentity();
+ if (!identity) return { success: false };
+
+ const verified = await confirmOtp.verify(data);
+ if (!verified.success) return { success: false };
+
+ const user = await db.users.update(identity.userId, {
+ email: verified.data.identifier,
+ });
+ if (!user) return { success: false };
+
+ return { success: true, viewer: user };
+ });
+
+/**
+ * Delete account
+ *
+ * Deletes the user and every session of theirs. Requires an active session.
+ */
+export const deleteAccountSF = createServerFn({ method: "POST" }).handler(
+ async () => {
+ const identity = await getIdentity();
+ if (!identity) return { success: false };
+
+ await db.sessions.deleteWhere({ userId: identity.userId });
+ await db.users.delete(identity.userId);
+ sessionCookie.clear();
+
+ return { success: true };
+ },
+);
+
+/**
+ * Server function: Sign out
+ *
+ * Ends the current session and clears the session cookie.
+ */
+export const signOut = createServerFn({ method: "POST" }).handler(async () => {
+ const token = sessionCookie.get();
+
+ if (token !== null) await sessionManager.end(token);
+ sessionCookie.clear();
+});
+
+/**
+ * Server function: Sign out all devices
+ *
+ * Deletes every session for the current user and clears the session cookie.
+ */
+export const signOutAll = createServerFn({ method: "POST" }).handler(
+ async () => {
+ const identity = await getIdentity();
+ if (identity) await db.sessions.deleteWhere({ userId: identity.userId });
+ sessionCookie.clear();
+ },
+);
+
+/**
+ * Server function: Get viewer
+ *
+ * Returns the current user if authenticated, or null otherwise.
+ */
+export const getViewer = createServerFn().handler(async () => {
+ const identity = await getIdentity();
+
+ return identity ? db.users.get(identity.userId) : null;
+});
diff --git a/examples/tanstack-start-react/kitchen-sink/src/auth.ts b/examples/tanstack-start-react/kitchen-sink/src/auth.ts
new file mode 100644
index 0000000..76e1e3e
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/src/auth.ts
@@ -0,0 +1,80 @@
+import {
+ makeOpaqueSessionManager,
+ makeOtpAuthFlow,
+ makeOTP,
+ recommendedOtpConfig,
+} from "@repo/spike";
+import { analytics } from "@repo/spike/demo";
+import { db } from "./db";
+
+/** How long someone stays signed in, in seconds. The cookie lives as long. */
+export const sessionTtl = 30 * 24 * 60 * 60;
+
+export const sessionManager = makeOpaqueSessionManager<{ userId: string }>({
+ store: async (token, row) => {
+ await db.sessions.insert({
+ id: token,
+ userId: row.userId,
+ expiresAt: row.expiresAt,
+ });
+ },
+ get: async (token) => {
+ const row = await db.sessions.get(token);
+
+ return row ? { userId: row.userId, expiresAt: row.expiresAt } : null;
+ },
+ delete: async (token) => {
+ await db.sessions.delete(token);
+ },
+ ttl: sessionTtl,
+});
+
+/** Confirms a new email address for someone who is signed in */
+export const confirmOtp = makeOTP({
+ purpose: "confirm-email",
+ store: async (row) => {
+ await db.otps.insert(row);
+ },
+ take: async (ticket) => db.otps.delete(ticket),
+ send: async ({ to, otp }) => {
+ console.log(`[OTP] Confirm new email, ${to}: ${otp}`);
+ },
+ ttl: 10 * 60,
+ attempts: 3,
+});
+
+/** Signs someone in or up by OTP, mounted in routes/api/auth/$.ts */
+export const otpAuthFlow = makeOtpAuthFlow({
+ ...recommendedOtpConfig,
+ storeOtp: async (row) => {
+ await db.otps.insert(row);
+ },
+ takeOtp: async (ticket) => db.otps.delete(ticket),
+ sendOtp: async ({ to, otp }) => {
+ console.log(`[OTP] Sign in, ${to}: ${otp}`);
+ },
+ sessionManager,
+ resolveUser: async (identifier) => {
+ const { row: user, isNew } = await db.users.findOrInsert(
+ { email: identifier },
+ { userId: crypto.randomUUID(), email: identifier },
+ );
+
+ return { success: true, user, isNew };
+ },
+ hooks: {
+ onSignIn: ({ user, isNew }) => {
+ analytics.track(isNew ? "sign_up" : "sign_in", user.userId);
+ },
+ },
+});
+
+/** Every origin the app's pages are served from */
+export const appOrigins = ["http://localhost:3100"];
+
+/** The session cookie the auth routes set */
+export const sessionCookieConfig = {
+ name: "session",
+ maxAge: sessionTtl,
+ secure: process.env.NODE_ENV === "production",
+};
diff --git a/examples/tanstack-start-react/kitchen-sink/src/db.ts b/examples/tanstack-start-react/kitchen-sink/src/db.ts
new file mode 100644
index 0000000..3f63e2e
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/src/db.ts
@@ -0,0 +1,26 @@
+/**
+ * In-memory database
+ *
+ * Simple in-memory stores for demonstration purposes. In a real app these
+ * would be replaced with database queries.
+ */
+import { makeMemoryTable } from "@repo/spike/demo";
+
+export const db = {
+ users: makeMemoryTable<{
+ userId: string;
+ email: string;
+ }>("userId"),
+ sessions: makeMemoryTable<{
+ id: string;
+ userId: string;
+ expiresAt: number;
+ }>("id"),
+ otps: makeMemoryTable<{
+ ticket: string;
+ identifier: string;
+ otp: string;
+ expiresAt: number;
+ attemptsLeft: number;
+ }>("ticket"),
+};
diff --git a/examples/tanstack-start-react/otp-memory/src/routeTree.gen.ts b/examples/tanstack-start-react/kitchen-sink/src/routeTree.gen.ts
similarity index 69%
rename from examples/tanstack-start-react/otp-memory/src/routeTree.gen.ts
rename to examples/tanstack-start-react/kitchen-sink/src/routeTree.gen.ts
index dceedff..363d6fd 100644
--- a/examples/tanstack-start-react/otp-memory/src/routeTree.gen.ts
+++ b/examples/tanstack-start-react/kitchen-sink/src/routeTree.gen.ts
@@ -10,33 +10,43 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'
+import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
+const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
+ id: '/api/auth/$',
+ path: '/api/auth/$',
+ getParentRoute: () => rootRouteImport,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
+ '/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
+ '/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
+ '/api/auth/$': typeof ApiAuthSplatRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/'
+ fullPaths: '/' | '/api/auth/$'
fileRoutesByTo: FileRoutesByTo
- to: '/'
- id: '__root__' | '/'
+ to: '/' | '/api/auth/$'
+ id: '__root__' | '/' | '/api/auth/$'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
+ ApiAuthSplatRoute: typeof ApiAuthSplatRoute
}
declare module '@tanstack/react-router' {
@@ -48,11 +58,19 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
+ '/api/auth/$': {
+ id: '/api/auth/$'
+ path: '/api/auth/$'
+ fullPath: '/api/auth/$'
+ preLoaderRoute: typeof ApiAuthSplatRouteImport
+ parentRoute: typeof rootRouteImport
+ }
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
+ ApiAuthSplatRoute: ApiAuthSplatRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/examples/tanstack-start-react/otp-memory/src/router.tsx b/examples/tanstack-start-react/kitchen-sink/src/router.tsx
similarity index 100%
rename from examples/tanstack-start-react/otp-memory/src/router.tsx
rename to examples/tanstack-start-react/kitchen-sink/src/router.tsx
diff --git a/examples/tanstack-start-react/otp-memory/src/routes/__root.tsx b/examples/tanstack-start-react/kitchen-sink/src/routes/__root.tsx
similarity index 90%
rename from examples/tanstack-start-react/otp-memory/src/routes/__root.tsx
rename to examples/tanstack-start-react/kitchen-sink/src/routes/__root.tsx
index 98af3b2..d4cdd61 100644
--- a/examples/tanstack-start-react/otp-memory/src/routes/__root.tsx
+++ b/examples/tanstack-start-react/kitchen-sink/src/routes/__root.tsx
@@ -6,7 +6,7 @@ export const Route = createRootRoute({
meta: [
{ charSet: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
- { title: "AuthAX - TanStack Start OTP demo" },
+ { title: "AuthAX - TanStack Start kitchen sink example" },
],
links: [{ rel: "stylesheet", href: appCss }],
}),
diff --git a/examples/tanstack-start-react/kitchen-sink/src/routes/api/auth/$.ts b/examples/tanstack-start-react/kitchen-sink/src/routes/api/auth/$.ts
new file mode 100644
index 0000000..ea11a9c
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/src/routes/api/auth/$.ts
@@ -0,0 +1,16 @@
+import { createFileRoute } from "@tanstack/react-router";
+import { serve } from "@repo/spike";
+import { appOrigins, otpAuthFlow, sessionCookieConfig } from "../../../auth";
+
+export const Route = createFileRoute("/api/auth/$")({
+ server: {
+ handlers: {
+ ANY: ({ request }) =>
+ serve(
+ request,
+ { ...otpAuthFlow.routes },
+ { cookie: sessionCookieConfig, origins: appOrigins },
+ ),
+ },
+ },
+});
diff --git a/examples/tanstack-start-react/otp-memory/src/routes/index.tsx b/examples/tanstack-start-react/kitchen-sink/src/routes/index.tsx
similarity index 77%
rename from examples/tanstack-start-react/otp-memory/src/routes/index.tsx
rename to examples/tanstack-start-react/kitchen-sink/src/routes/index.tsx
index e5f5f54..019c358 100644
--- a/examples/tanstack-start-react/otp-memory/src/routes/index.tsx
+++ b/examples/tanstack-start-react/kitchen-sink/src/routes/index.tsx
@@ -1,9 +1,10 @@
import { useState } from "react";
import { createFileRoute, useRouter } from "@tanstack/react-router";
+import { makeAuthClient } from "@repo/spike";
import {
- requestOtp,
- verifyOtp,
+ requestChangeEmail,
changeEmail,
+ deleteAccountSF,
signOut,
signOutAll,
getViewer,
@@ -27,9 +28,12 @@ export const Route = createFileRoute("/")({
type Viewer = { userId: string; email: string };
+const authax = makeAuthClient("/api/auth");
+
function AuthFlow(props: { onSignedIn: () => void }) {
const [step, setStep] = useState<"email" | "otp">("email");
const [email, setEmail] = useState("");
+ const [ticket, setTicket] = useState("");
const [otp, setOtp] = useState("");
const [error, setError] = useState(null);
@@ -39,8 +43,9 @@ function AuthFlow(props: { onSignedIn: () => void }) {
as="form"
onSubmit={async (e) => {
e.preventDefault();
- const result = await requestOtp({ data: { identifier: email } });
+ const result = await authax.otpSend(email);
if (result.success) {
+ setTicket(result.ticket);
setStep("otp");
setError(null);
} else {
@@ -65,9 +70,7 @@ function AuthFlow(props: { onSignedIn: () => void }) {
as="form"
onSubmit={async (e) => {
e.preventDefault();
- const result = await verifyOtp({
- data: { identifier: email, otp },
- });
+ const result = await authax.otpVerify({ ticket, otp });
if (result.success) {
props.onSignedIn();
} else {
@@ -82,9 +85,7 @@ function AuthFlow(props: { onSignedIn: () => void }) {
@@ -92,9 +93,15 @@ function AuthFlow(props: { onSignedIn: () => void }) {
variant="secondary"
type="button"
onClick={async () => {
- await requestOtp({ data: { identifier: email } });
- setOtp("");
- setError(null);
+ const result = await authax.otpSend(email);
+
+ if (result.success) {
+ setTicket(result.ticket);
+ setOtp("");
+ setError(null);
+ } else {
+ setError("Failed to send one-time password");
+ }
}}
>
Send a new one-time password
@@ -106,6 +113,7 @@ function AuthFlow(props: { onSignedIn: () => void }) {
function ChangeEmailFlow(props: { onDone: () => void; onCancel: () => void }) {
const [step, setStep] = useState<"email" | "otp">("email");
const [email, setEmail] = useState("");
+ const [ticket, setTicket] = useState("");
const [otp, setOtp] = useState("");
const [error, setError] = useState(null);
@@ -115,8 +123,12 @@ function ChangeEmailFlow(props: { onDone: () => void; onCancel: () => void }) {
as="form"
onSubmit={async (e) => {
e.preventDefault();
- const result = await requestOtp({ data: { identifier: email } });
+ const result = await requestChangeEmail({
+ data: { identifier: email },
+ });
+
if (result.success) {
+ setTicket(result.ticket);
setStep("otp");
setError(null);
} else {
@@ -148,7 +160,7 @@ function ChangeEmailFlow(props: { onDone: () => void; onCancel: () => void }) {
onSubmit={async (e) => {
e.preventDefault();
const result = await changeEmail({
- data: { identifier: email, otp },
+ data: { ticket, otp },
});
if (result.success) {
props.onDone();
@@ -164,9 +176,7 @@ function ChangeEmailFlow(props: { onDone: () => void; onCancel: () => void }) {
@@ -174,9 +184,17 @@ function ChangeEmailFlow(props: { onDone: () => void; onCancel: () => void }) {
variant="secondary"
type="button"
onClick={async () => {
- await requestOtp({ data: { identifier: email } });
- setOtp("");
- setError(null);
+ const result = await requestChangeEmail({
+ data: { identifier: email },
+ });
+
+ if (result.success) {
+ setTicket(result.ticket);
+ setOtp("");
+ setError(null);
+ } else {
+ setError("Failed to send one-time password");
+ }
}}
>
Send a new one-time password
@@ -230,6 +248,15 @@ function Authenticated(props: {
>
Sign out all devices
+
);
}
@@ -239,7 +266,7 @@ function App() {
const router = useRouter();
return (
-
+
{viewer ? (
getCookie(name) ?? null,
- set: (token: string, expiresAt: Date) =>
- setCookie(name, token, { ...options, expires: expiresAt }),
+ set: (token: string) =>
+ setCookie(name, token, { ...options, maxAge: sessionTtl }),
clear: () => setCookie(name, "", { ...options, maxAge: 0 }),
};
diff --git a/examples/tanstack-start-react/kitchen-sink/src/styles.css b/examples/tanstack-start-react/kitchen-sink/src/styles.css
new file mode 100644
index 0000000..3306d99
--- /dev/null
+++ b/examples/tanstack-start-react/kitchen-sink/src/styles.css
@@ -0,0 +1,2 @@
+@import "tailwindcss";
+@import "@repo/shared-react/styles.css";
diff --git a/examples/tanstack-start-react/otp-memory/tsconfig.json b/examples/tanstack-start-react/kitchen-sink/tsconfig.json
similarity index 100%
rename from examples/tanstack-start-react/otp-memory/tsconfig.json
rename to examples/tanstack-start-react/kitchen-sink/tsconfig.json
diff --git a/examples/tanstack-start-react/otp-memory/vite.config.ts b/examples/tanstack-start-react/kitchen-sink/vite.config.ts
similarity index 100%
rename from examples/tanstack-start-react/otp-memory/vite.config.ts
rename to examples/tanstack-start-react/kitchen-sink/vite.config.ts
diff --git a/examples/tanstack-start-react/otp-memory/src/auth-rpc.ts b/examples/tanstack-start-react/otp-memory/src/auth-rpc.ts
deleted file mode 100644
index 826a1e2..0000000
--- a/examples/tanstack-start-react/otp-memory/src/auth-rpc.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { createServerFn } from "@tanstack/react-start";
-import { z } from "zod";
-import { db } from "./db";
-import { auth, emailOtp } from "./auth";
-import { sessionCookie } from "./session-cookie";
-
-/**
- * Request OTP schema
- */
-export const requestOtpSchema = z.object({
- identifier: z.email(),
-});
-
-/**
- * Verify OTP schema
- */
-export const verifyOtpSchema = z.object({
- identifier: z.email(),
- otp: z.string().length(6),
-});
-
-/**
- * Send OTP to identifier server function
- */
-export const requestOtp = createServerFn({ method: "POST" })
- .validator(requestOtpSchema)
- .handler(({ data }) => auth.strategies.email.request(data));
-
-/**
- * Verify OTP server function
- *
- * Authenticates with the OTP, which upserts the user and establishes a
- * session. Returns isNew to distinguish sign-up from sign-in (for analytics,
- * onboarding, etc.).
- */
-export const verifyOtp = createServerFn({ method: "POST" })
- .validator(verifyOtpSchema)
- .handler(async ({ data }) => {
- const result = await auth.strategies.email.authenticate(data);
-
- if (!result.success) return { success: false };
-
- sessionCookie.set(result.data.session.token, result.data.session.expiresAt);
-
- return { success: true, isNew: result.data.user.isNew };
- });
-
-/**
- * Change email
- *
- * Verifies OTP for the new email, then swaps it on the authenticated user.
- * Requires an active session — the OTP proves ownership of the new address.
- */
-export const changeEmail = createServerFn({ method: "POST" })
- .validator(verifyOtpSchema)
- .handler(async ({ data }) => {
- const identity = await auth.session.get(sessionCookie.get());
- if (!identity) return { success: false };
-
- const verified = await emailOtp.verify(data.identifier, data.otp);
- if (!verified) return { success: false };
-
- const user = db.users.updateEmail(identity.userId, data.identifier);
- if (!user) return { success: false };
-
- return { success: true, viewer: user };
- });
-
-/**
- * Server function: Sign out
- *
- * Ends the current session and clears the session cookie.
- */
-export const signOut = createServerFn({ method: "POST" }).handler(async () => {
- await auth.session.end(sessionCookie.get());
- sessionCookie.clear();
-});
-
-/**
- * Server function: Sign out all devices
- *
- * Deletes every session for the current user and clears the session cookie.
- */
-export const signOutAll = createServerFn({ method: "POST" }).handler(
- async () => {
- const identity = await auth.session.get(sessionCookie.get());
- if (identity) db.sessions.deleteAllForUser(identity.userId);
- sessionCookie.clear();
- },
-);
-
-/**
- * Server function: Get viewer
- *
- * Returns the current user if authenticated, or null otherwise.
- */
-export const getViewer = createServerFn().handler(async () => {
- const identity = await auth.session.get(sessionCookie.get());
-
- return identity ? (db.users.get(identity.userId) ?? null) : null;
-});
diff --git a/examples/tanstack-start-react/otp-memory/src/auth.ts b/examples/tanstack-start-react/otp-memory/src/auth.ts
deleted file mode 100644
index 05f9ba1..0000000
--- a/examples/tanstack-start-react/otp-memory/src/auth.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { makeAuth, makeOpaqueSession, makeOtp, makeOtpStrategy } from "authax";
-import { db } from "./db";
-
-const session = makeOpaqueSession({
- storage: db.sessions,
- ttl: 30 * 24 * 60 * 60 * 1000,
-});
-
-export const emailOtp = makeOtp({
- storage: db.otps,
- delivery: {
- send: async (identifier, otp) => {
- console.log(`[OTP] ${identifier}: ${otp}`);
- },
- },
- ttl: 10 * 60 * 1000,
- attempts: 3,
-});
-
-export const auth = makeAuth(session, (kernel) => ({
- email: makeOtpStrategy(kernel, {
- request: async ({ identifier }) => {
- await emailOtp.request(identifier);
- return { success: true };
- },
- authenticate: async ({ identifier, otp }) => {
- if (!(await emailOtp.verify(identifier, otp))) {
- return { success: false, error: "invalid_otp" };
- }
-
- return { success: true, data: db.users.upsert(identifier) };
- },
- }),
-}));
diff --git a/examples/tanstack-start-react/otp-memory/src/db.ts b/examples/tanstack-start-react/otp-memory/src/db.ts
deleted file mode 100644
index 5411432..0000000
--- a/examples/tanstack-start-react/otp-memory/src/db.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * In-memory database
- *
- * Simple in-memory stores for demonstration purposes. In a real app these
- * would be replaced with database queries.
- */
-import type { OtpRecord, SessionRecord } from "authax";
-
-const users = new Map();
-let userIdCounter = 0;
-
-const sessions = new Map();
-const otps = new Map();
-
-export const db = {
- users: {
- upsert: (email: string) => {
- const exists = Array.from(users.values()).find((u) => u.email === email);
-
- if (exists) {
- return { userId: exists.userId, isNew: false };
- }
-
- const userId = `user_${++userIdCounter}`;
- users.set(userId, { userId, email });
-
- return { userId, isNew: true };
- },
-
- get: (userId: string) => users.get(userId),
-
- updateEmail: (userId: string, email: string) => {
- const user = users.get(userId);
- if (!user) return undefined;
- user.email = email;
- return user;
- },
- },
-
- sessions: {
- store: async (record: SessionRecord) => {
- sessions.set(record.sessionId, record);
- },
-
- get: async (sessionId: string) => sessions.get(sessionId) ?? null,
-
- delete: async (sessionId: string) => {
- sessions.delete(sessionId);
- },
-
- deleteAllForUser: (userId: string) => {
- for (const [sessionId, record] of sessions) {
- if (record.userId === userId) {
- sessions.delete(sessionId);
- }
- }
- },
- },
-
- otps: {
- store: async (record: OtpRecord) => {
- otps.set(record.identifier, record);
- },
-
- take: async (identifier: string) => {
- const record = otps.get(identifier) ?? null;
- otps.delete(identifier);
- return record;
- },
- },
-};
diff --git a/examples/tanstack-start-react/otp-memory/src/styles.css b/examples/tanstack-start-react/otp-memory/src/styles.css
deleted file mode 100644
index 017b778..0000000
--- a/examples/tanstack-start-react/otp-memory/src/styles.css
+++ /dev/null
@@ -1,10 +0,0 @@
-@import "tailwindcss";
-@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/tanstack-start-react/otp-passkey-memory/package.json b/examples/tanstack-start-react/otp-passkey-memory/package.json
index 410df1c..d39c705 100644
--- a/examples/tanstack-start-react/otp-passkey-memory/package.json
+++ b/examples/tanstack-start-react/otp-passkey-memory/package.json
@@ -13,8 +13,8 @@
"authax": "workspace:*",
"@repo/shared-react": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
@@ -22,11 +22,11 @@
"@repo/shared-webauthn": "workspace:*"
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1"
+ "vite": "^8.3.2"
}
}
diff --git a/examples/tanstack-start-react/otp-passkey-memory/src/styles.css b/examples/tanstack-start-react/otp-passkey-memory/src/styles.css
index 017b778..3306d99 100644
--- a/examples/tanstack-start-react/otp-passkey-memory/src/styles.css
+++ b/examples/tanstack-start-react/otp-passkey-memory/src/styles.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/tanstack-start-react/otp-passkey-memory/src/use-passkey.ts b/examples/tanstack-start-react/otp-passkey-memory/src/use-passkey.ts
index dbb95f6..a8659b1 100644
--- a/examples/tanstack-start-react/otp-passkey-memory/src/use-passkey.ts
+++ b/examples/tanstack-start-react/otp-passkey-memory/src/use-passkey.ts
@@ -82,7 +82,7 @@ export function usePasskeyAuthentication(
const start = await options.start();
if (!start.success) {
- setError("Failed to start sign-in");
+ setError("Failed to start authentication");
setLoading(false);
return;
}
@@ -90,7 +90,7 @@ export function usePasskeyAuthentication(
const credential = await getPasskey(start.options);
if (!credential) {
- setError("Passkey sign-in was cancelled");
+ setError("Passkey authentication was cancelled");
setLoading(false);
return;
}
@@ -100,11 +100,11 @@ export function usePasskeyAuthentication(
if (result.success) {
options.onSuccess();
} else {
- setError("Failed to sign in");
+ setError("Failed to authenticate with passkey");
}
} catch (cause) {
- console.error("Passkey sign-in failed:", cause);
- setError("Passkey sign-in failed");
+ console.error("Passkey authentication failed:", cause);
+ setError("Passkey authentication failed");
}
setLoading(false);
diff --git a/examples/tanstack-start-react/otp-passkey-strict-memory/package.json b/examples/tanstack-start-react/otp-passkey-strict-memory/package.json
index 1388c80..fa630f8 100644
--- a/examples/tanstack-start-react/otp-passkey-strict-memory/package.json
+++ b/examples/tanstack-start-react/otp-passkey-strict-memory/package.json
@@ -13,8 +13,8 @@
"authax": "workspace:*",
"@repo/shared-react": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
@@ -22,11 +22,11 @@
"@repo/shared-webauthn": "workspace:*"
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1"
+ "vite": "^8.3.2"
}
}
diff --git a/examples/tanstack-start-react/otp-passkey-strict-memory/src/styles.css b/examples/tanstack-start-react/otp-passkey-strict-memory/src/styles.css
index 017b778..3306d99 100644
--- a/examples/tanstack-start-react/otp-passkey-strict-memory/src/styles.css
+++ b/examples/tanstack-start-react/otp-passkey-strict-memory/src/styles.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/tanstack-start-react/otp-passkey-strict-memory/src/use-passkey.ts b/examples/tanstack-start-react/otp-passkey-strict-memory/src/use-passkey.ts
index dbb95f6..a8659b1 100644
--- a/examples/tanstack-start-react/otp-passkey-strict-memory/src/use-passkey.ts
+++ b/examples/tanstack-start-react/otp-passkey-strict-memory/src/use-passkey.ts
@@ -82,7 +82,7 @@ export function usePasskeyAuthentication(
const start = await options.start();
if (!start.success) {
- setError("Failed to start sign-in");
+ setError("Failed to start authentication");
setLoading(false);
return;
}
@@ -90,7 +90,7 @@ export function usePasskeyAuthentication(
const credential = await getPasskey(start.options);
if (!credential) {
- setError("Passkey sign-in was cancelled");
+ setError("Passkey authentication was cancelled");
setLoading(false);
return;
}
@@ -100,11 +100,11 @@ export function usePasskeyAuthentication(
if (result.success) {
options.onSuccess();
} else {
- setError("Failed to sign in");
+ setError("Failed to authenticate with passkey");
}
} catch (cause) {
- console.error("Passkey sign-in failed:", cause);
- setError("Passkey sign-in failed");
+ console.error("Passkey authentication failed:", cause);
+ setError("Passkey authentication failed");
}
setLoading(false);
diff --git a/examples/tanstack-start-react/passkey-memory/package.json b/examples/tanstack-start-react/passkey-memory/package.json
index 9ffa4db..03dbd04 100644
--- a/examples/tanstack-start-react/passkey-memory/package.json
+++ b/examples/tanstack-start-react/passkey-memory/package.json
@@ -10,23 +10,22 @@
"typecheck": "tsc"
},
"dependencies": {
- "authax": "workspace:*",
"@repo/shared-react": "workspace:*",
+ "@repo/spike": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
- "zod": "^4.6.5",
- "@repo/shared-webauthn": "workspace:*"
+ "zod": "^4.6.5"
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1"
+ "vite": "^8.3.2"
}
}
diff --git a/examples/tanstack-start-react/passkey-memory/src/auth-rpc.ts b/examples/tanstack-start-react/passkey-memory/src/auth-rpc.ts
index b87b3bc..368ed29 100644
--- a/examples/tanstack-start-react/passkey-memory/src/auth-rpc.ts
+++ b/examples/tanstack-start-react/passkey-memory/src/auth-rpc.ts
@@ -1,13 +1,35 @@
import { createServerFn } from "@tanstack/react-start";
-import {
- passkeyAuthenticationCredentialSchema,
- passkeyRegistrationCredentialSchema,
-} from "@repo/shared-webauthn";
+import { analytics } from "@repo/spike/demo";
import { z } from "zod";
import { db } from "./db";
-import { auth } from "./auth";
+import { passkey, sessionManager } from "./auth";
import { sessionCookie } from "./session-cookie";
+/** The session behind the request's cookie, null when there is none */
+async function getIdentity() {
+ const token = sessionCookie.get();
+
+ return token === null ? null : sessionManager.get(token);
+}
+
+/** What the browser sends after it created a passkey */
+const registrationCredentialSchema = z.object({
+ response: z.object({
+ clientDataJSON: z.string(),
+ attestationObject: z.string(),
+ }),
+});
+
+/** What the browser sends after it signed in with a passkey */
+const authenticationCredentialSchema = z.object({
+ id: z.string(),
+ response: z.object({
+ clientDataJSON: z.string(),
+ authenticatorData: z.string(),
+ signature: z.string(),
+ }),
+});
+
/**
* Server function: Start passkey registration
*
@@ -15,13 +37,13 @@ import { sessionCookie } from "./session-cookie";
* application user is provisioned only after the ceremony verifies.
*/
export const startRegistration = createServerFn({ method: "POST" }).handler(
- async () => {
- const result = await auth.strategies.passkeys.createRegistrationOptions();
-
- if (!result.success) return { success: false as const };
-
- return { success: true as const, options: result.data };
- },
+ async () => ({
+ success: true as const,
+ options: await passkey.beginRegistration({
+ handle: crypto.randomUUID(),
+ name: "New user",
+ }),
+ }),
);
/**
@@ -31,15 +53,24 @@ export const startRegistration = createServerFn({ method: "POST" }).handler(
* establishes a session.
*/
export const verifyRegistration = createServerFn({ method: "POST" })
- .validator(z.object({ credential: passkeyRegistrationCredentialSchema }))
+ .validator(z.object({ credential: registrationCredentialSchema }))
.handler(async ({ data }) => {
- const result = await auth.strategies.passkeys.verifyRegistration({
- credential: data.credential,
- });
+ const result = await passkey.finishRegistration(data.credential);
+
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
- if (!result.success) return { success: false as const };
+ return { success: false as const };
+ }
- sessionCookie.set(result.data.session.token, result.data.session.expiresAt);
+ const userId = result.data.userHandle;
+ const { isNew } = await db.users.findOrInsert({ userId }, { userId });
+
+ // A ceremony that was started to add a passkey signs nobody up
+ if (!isNew) return { success: false as const };
+
+ sessionCookie.set(await sessionManager.make({ userId }));
+ analytics.track("sign_up", userId);
return { success: true as const };
});
@@ -52,14 +83,16 @@ export const verifyRegistration = createServerFn({ method: "POST" })
*/
export const startAddPasskey = createServerFn({ method: "POST" }).handler(
async () => {
- const result =
- await auth.strategies.passkeys.createAdditionalRegistrationOptions(
- sessionCookie.get(),
- );
-
- if (!result.success) return { success: false as const };
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
- return { success: true as const, options: result.data };
+ return {
+ success: true as const,
+ options: await passkey.beginRegistration({
+ handle: identity.userId,
+ name: identity.userId,
+ }),
+ };
},
);
@@ -70,14 +103,26 @@ export const startAddPasskey = createServerFn({ method: "POST" }).handler(
* for the current user. No session is established.
*/
export const verifyAddPasskey = createServerFn({ method: "POST" })
- .validator(z.object({ credential: passkeyRegistrationCredentialSchema }))
+ .validator(z.object({ credential: registrationCredentialSchema }))
.handler(async ({ data }) => {
- const result = await auth.strategies.passkeys.verifyAdditionalRegistration(
- sessionCookie.get(),
- { credential: data.credential },
- );
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
+
+ const result = await passkey.finishRegistration(data.credential);
+
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
+
+ return { success: false as const };
+ }
+
+ // The ceremony has to be one this user started. The passkey is already
+ // stored when the answer comes back, so it is removed again.
+ if (result.data.userHandle !== identity.userId) {
+ await db.credentials.delete(result.data.credentialId);
- if (!result.success) return { success: false as const };
+ return { success: false as const };
+ }
return { success: true as const };
});
@@ -89,17 +134,10 @@ export const verifyAddPasskey = createServerFn({ method: "POST" })
*/
export const startAuthentication = createServerFn({
method: "POST",
-}).handler(async () => {
- const result = await auth.strategies.passkeys.createAuthenticationOptions();
- return { success: true as const, options: result.data };
-});
-
-/**
- * Verify passkey authentication schema
- */
-const verifyAuthenticationSchema = z.object({
- credential: passkeyAuthenticationCredentialSchema,
-});
+}).handler(async () => ({
+ success: true as const,
+ options: await passkey.beginAuthentication(),
+}));
/**
* Verify passkey authentication
@@ -108,15 +146,20 @@ const verifyAuthenticationSchema = z.object({
* establishes a session.
*/
export const verifyAuthentication = createServerFn({ method: "POST" })
- .validator(verifyAuthenticationSchema)
+ .validator(z.object({ credential: authenticationCredentialSchema }))
.handler(async ({ data }) => {
- const result = await auth.strategies.passkeys.verifyAuthentication({
- credential: data.credential,
- });
+ const result = await passkey.finishAuthentication(data.credential);
- if (!result.success) return { success: false as const };
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
- sessionCookie.set(result.data.session.token, result.data.session.expiresAt);
+ return { success: false as const };
+ }
+
+ sessionCookie.set(
+ await sessionManager.make({ userId: result.data.userHandle }),
+ );
+ analytics.track("sign_in", result.data.userHandle);
return { success: true as const };
});
@@ -127,12 +170,12 @@ export const verifyAuthentication = createServerFn({ method: "POST" })
* Returns stored credential metadata for the authenticated user.
*/
export const listPasskeys = createServerFn().handler(async () => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
if (!identity) return { passkeys: [] };
- const passkeys = await db.credentials.list(identity.userId);
+ const passkeys = await db.credentials.where({ userId: identity.userId });
return {
- passkeys: passkeys.map((p) => ({ id: p.credentialId })),
+ passkeys: passkeys.map((p) => ({ id: p.id })),
};
});
@@ -144,16 +187,68 @@ export const listPasskeys = createServerFn().handler(async () => {
export const removePasskey = createServerFn({ method: "POST" })
.validator(z.object({ credentialId: z.string() }))
.handler(async ({ data }) => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
if (!identity) return { success: false as const };
- const passkeys = await db.credentials.list(identity.userId);
+ const passkeys = await db.credentials.where({ userId: identity.userId });
if (passkeys.length <= 1) return { success: false as const };
- const owns = passkeys.some((p) => p.credentialId === data.credentialId);
+ const owns = passkeys.some((p) => p.id === data.credentialId);
if (!owns) return { success: false as const };
- db.credentials.delete(data.credentialId);
+ await db.credentials.delete(data.credentialId);
+ return { success: true as const };
+ });
+
+/**
+ * Start deleting the account server function
+ *
+ * Requires an active session. Returns WebAuthn authentication options, so the
+ * person confirms with a passkey.
+ */
+export const requestDeleteAccountSF = createServerFn({
+ method: "POST",
+}).handler(async () => {
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
+
+ return {
+ success: true as const,
+ options: await passkey.beginAuthentication(),
+ };
+});
+
+/**
+ * Delete account
+ *
+ * Verifies the passkey assertion, then deletes the user, their passkeys, and
+ * every session of theirs. Requires an active session.
+ */
+export const deleteAccountSF = createServerFn({ method: "POST" })
+ .validator(z.object({ credential: authenticationCredentialSchema }))
+ .handler(async ({ data }) => {
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
+
+ const result = await passkey.finishAuthentication(data.credential);
+
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
+
+ return { success: false as const };
+ }
+
+ // Verification succeeds for any stored passkey, so the passkey has to
+ // belong to the user in the session
+ if (result.data.userHandle !== identity.userId) {
+ return { success: false as const };
+ }
+
+ await db.credentials.deleteWhere({ userId: identity.userId });
+ await db.sessions.deleteWhere({ userId: identity.userId });
+ await db.users.delete(identity.userId);
+ sessionCookie.clear();
+
return { success: true as const };
});
@@ -163,7 +258,9 @@ export const removePasskey = createServerFn({ method: "POST" })
* Ends the current session and clears the session cookie.
*/
export const signOut = createServerFn({ method: "POST" }).handler(async () => {
- await auth.session.end(sessionCookie.get());
+ const token = sessionCookie.get();
+
+ if (token !== null) await sessionManager.end(token);
sessionCookie.clear();
});
@@ -174,8 +271,8 @@ export const signOut = createServerFn({ method: "POST" }).handler(async () => {
*/
export const signOutAll = createServerFn({ method: "POST" }).handler(
async () => {
- const identity = await auth.session.get(sessionCookie.get());
- if (identity) db.sessions.deleteAllForUser(identity.userId);
+ const identity = await getIdentity();
+ if (identity) await db.sessions.deleteWhere({ userId: identity.userId });
sessionCookie.clear();
},
);
@@ -186,7 +283,7 @@ export const signOutAll = createServerFn({ method: "POST" }).handler(
* Returns the current user if authenticated, or null otherwise.
*/
export const getViewer = createServerFn().handler(async () => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
- return identity ? (db.users.get(identity.userId) ?? null) : null;
+ return identity ? db.users.get(identity.userId) : null;
});
diff --git a/examples/tanstack-start-react/passkey-memory/src/auth.ts b/examples/tanstack-start-react/passkey-memory/src/auth.ts
index 78bd7d2..de9539b 100644
--- a/examples/tanstack-start-react/passkey-memory/src/auth.ts
+++ b/examples/tanstack-start-react/passkey-memory/src/auth.ts
@@ -1,30 +1,61 @@
-import {
- makeAuth,
- makeOpaqueSession,
- makePasskeyEngine,
- makePasskeyStrategy,
-} from "authax";
+import { makeOpaqueSessionManager, makePasskey } from "@repo/spike";
import { db } from "./db";
-const session = makeOpaqueSession({
- storage: db.sessions,
- ttl: 30 * 24 * 60 * 60 * 1000,
-});
+/** How long someone stays signed in, in seconds. The cookie lives as long. */
+export const sessionTtl = 30 * 24 * 60 * 60;
+
+export const sessionManager = makeOpaqueSessionManager<{ userId: string }>({
+ store: async (token, row) => {
+ await db.sessions.insert({
+ id: token,
+ userId: row.userId,
+ expiresAt: row.expiresAt,
+ });
+ },
+ get: async (token) => {
+ const row = await db.sessions.get(token);
-const passkey = makePasskeyEngine({
- storage: db.credentials,
- challenge: { storage: db.challenges, ttl: 5 * 60 * 1000 },
- webAuthn: {
- rpId: "localhost",
- rpName: "Auth Passkey Demo",
- allowedOrigins: ["http://localhost:3107"],
- },
- displayName: async (context) =>
- context.intent === "add" ? context.userId : "New user",
- signUp: async () => db.users.create().userId,
- debug: true,
+ return row ? { userId: row.userId, expiresAt: row.expiresAt } : null;
+ },
+ delete: async (token) => {
+ await db.sessions.delete(token);
+ },
+ ttl: sessionTtl,
});
-export const auth = makeAuth(session, (kernel) => ({
- passkeys: makePasskeyStrategy(kernel, passkey),
-}));
+export const passkey = makePasskey({
+ rpId: "localhost",
+ rpName: "Auth Passkey Demo",
+ origins: ["http://localhost:3107"],
+ ttl: 5 * 60,
+ storeChallenge: async (challenge, row) => {
+ await db.challenges.insert({
+ id: challenge,
+ userId: row.handle,
+ expiresAt: row.expiresAt,
+ });
+ },
+ takeChallenge: async (challenge) => {
+ const row = await db.challenges.delete(challenge);
+
+ return row ? { handle: row.userId, expiresAt: row.expiresAt } : null;
+ },
+ storeCredential: async (credentialId, row) => {
+ await db.credentials.insert({
+ id: credentialId,
+ userId: row.handle,
+ publicKey: row.publicKey,
+ counter: row.counter,
+ });
+ },
+ getCredential: async (credentialId) => {
+ const row = await db.credentials.get(credentialId);
+
+ return row
+ ? { handle: row.userId, publicKey: row.publicKey, counter: row.counter }
+ : null;
+ },
+ setCounter: async (credentialId, counter) => {
+ await db.credentials.update(credentialId, { counter });
+ },
+});
diff --git a/examples/tanstack-start-react/passkey-memory/src/db.ts b/examples/tanstack-start-react/passkey-memory/src/db.ts
index 5043359..644297c 100644
--- a/examples/tanstack-start-react/passkey-memory/src/db.ts
+++ b/examples/tanstack-start-react/passkey-memory/src/db.ts
@@ -4,74 +4,30 @@
* Simple in-memory stores for demonstration purposes. In a real app these
* would be replaced with database queries.
*/
-import type { ChallengeRecord, CredentialRecord, SessionRecord } from "authax";
-
-const users = new Map();
-let userIdCounter = 0;
-
-const sessions = new Map();
-const credentials = new Map();
-const challenges = new Map();
+import { makeMemoryTable } from "@repo/spike/demo";
export const db = {
- users: {
- create: () => {
- const userId = `user_${++userIdCounter}`;
- users.set(userId, { userId });
- return { userId };
- },
- get: (userId: string) => users.get(userId),
- },
-
- sessions: {
- store: async (record: SessionRecord) => {
- sessions.set(record.sessionId, record);
- },
-
- get: async (sessionId: string) => sessions.get(sessionId) ?? null,
-
- delete: async (sessionId: string) => {
- sessions.delete(sessionId);
- },
-
- deleteAllForUser: (userId: string) => {
- for (const [sessionId, record] of sessions) {
- if (record.userId === userId) {
- sessions.delete(sessionId);
- }
- }
- },
- },
-
- credentials: {
- store: async (record: CredentialRecord) => {
- credentials.set(record.credentialId, record);
- },
-
- get: async (credentialId: string) => credentials.get(credentialId) ?? null,
-
- list: async (userId: string) =>
- Array.from(credentials.values()).filter((c) => c.userId === userId),
-
- setCounter: async (credentialId: string, counter: number) => {
- const record = credentials.get(credentialId);
- if (record) credentials.set(credentialId, { ...record, counter });
- },
-
- delete: (credentialId: string) => {
- credentials.delete(credentialId);
- },
- },
-
- challenges: {
- store: async (record: ChallengeRecord) => {
- challenges.set(record.challenge, record);
- },
-
- take: async (challenge: string) => {
- const record = challenges.get(challenge) ?? null;
- challenges.delete(challenge);
- return record;
- },
- },
+ users: makeMemoryTable<{
+ userId: string;
+ }>("userId"),
+
+ sessions: makeMemoryTable<{
+ id: string;
+ userId: string;
+ expiresAt: number;
+ }>("id"),
+
+ credentials: makeMemoryTable<{
+ id: string;
+ userId: string;
+ publicKey: string;
+ counter: number;
+ }>("id"),
+
+ challenges: makeMemoryTable<{
+ id: string;
+ /** Who a registration is for. Null when the challenge is for signing in. */
+ userId: string | null;
+ expiresAt: number;
+ }>("id"),
};
diff --git a/examples/tanstack-start-react/passkey-memory/src/routes/index.tsx b/examples/tanstack-start-react/passkey-memory/src/routes/index.tsx
index 08eb811..05ed253 100644
--- a/examples/tanstack-start-react/passkey-memory/src/routes/index.tsx
+++ b/examples/tanstack-start-react/passkey-memory/src/routes/index.tsx
@@ -8,6 +8,8 @@ import {
verifyAddPasskey,
listPasskeys,
removePasskey,
+ requestDeleteAccountSF,
+ deleteAccountSF,
signOut,
signOutAll,
getViewer,
@@ -86,6 +88,12 @@ function Authenticated(props: {
onSuccess: () => props.onChanged(),
});
+ const deleteAccount = usePasskeyAuthentication({
+ start: () => requestDeleteAccountSF(),
+ verify: (credential) => deleteAccountSF({ data: { credential } }),
+ onSuccess: () => props.onChanged(),
+ });
+
const handleRemovePasskey = async (credentialId: string) => {
const result = await removePasskey({ data: { credentialId } });
if (result.success) {
@@ -125,6 +133,17 @@ function Authenticated(props: {
>
Sign out all devices
+
+
+ {deleteAccount.error !== null ? (
+ {deleteAccount.error}
+ ) : null}
);
}
diff --git a/examples/tanstack-start-react/passkey-memory/src/session-cookie.ts b/examples/tanstack-start-react/passkey-memory/src/session-cookie.ts
index 60eb30c..4f99d5a 100644
--- a/examples/tanstack-start-react/passkey-memory/src/session-cookie.ts
+++ b/examples/tanstack-start-react/passkey-memory/src/session-cookie.ts
@@ -1,4 +1,5 @@
import { getCookie, setCookie } from "@tanstack/react-start/server";
+import { sessionTtl } from "./auth";
const name = "session";
@@ -12,7 +13,7 @@ const options = {
/** Moves the session token between the request and the auth API */
export const sessionCookie = {
get: () => getCookie(name) ?? null,
- set: (token: string, expiresAt: Date) =>
- setCookie(name, token, { ...options, expires: expiresAt }),
+ set: (token: string) =>
+ setCookie(name, token, { ...options, maxAge: sessionTtl }),
clear: () => setCookie(name, "", { ...options, maxAge: 0 }),
};
diff --git a/examples/tanstack-start-react/passkey-memory/src/styles.css b/examples/tanstack-start-react/passkey-memory/src/styles.css
index 017b778..3306d99 100644
--- a/examples/tanstack-start-react/passkey-memory/src/styles.css
+++ b/examples/tanstack-start-react/passkey-memory/src/styles.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/tanstack-start-react/passkey-memory/src/use-passkey.ts b/examples/tanstack-start-react/passkey-memory/src/use-passkey.ts
index dbb95f6..a8659b1 100644
--- a/examples/tanstack-start-react/passkey-memory/src/use-passkey.ts
+++ b/examples/tanstack-start-react/passkey-memory/src/use-passkey.ts
@@ -82,7 +82,7 @@ export function usePasskeyAuthentication(
const start = await options.start();
if (!start.success) {
- setError("Failed to start sign-in");
+ setError("Failed to start authentication");
setLoading(false);
return;
}
@@ -90,7 +90,7 @@ export function usePasskeyAuthentication(
const credential = await getPasskey(start.options);
if (!credential) {
- setError("Passkey sign-in was cancelled");
+ setError("Passkey authentication was cancelled");
setLoading(false);
return;
}
@@ -100,11 +100,11 @@ export function usePasskeyAuthentication(
if (result.success) {
options.onSuccess();
} else {
- setError("Failed to sign in");
+ setError("Failed to authenticate with passkey");
}
} catch (cause) {
- console.error("Passkey sign-in failed:", cause);
- setError("Passkey sign-in failed");
+ console.error("Passkey authentication failed:", cause);
+ setError("Passkey authentication failed");
}
setLoading(false);
diff --git a/examples/tanstack-start-react/passkey-otp-memory/package.json b/examples/tanstack-start-react/passkey-otp-memory/package.json
index f0ae329..a2863dd 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/package.json
+++ b/examples/tanstack-start-react/passkey-otp-memory/package.json
@@ -10,23 +10,22 @@
"typecheck": "tsc"
},
"dependencies": {
- "authax": "workspace:*",
"@repo/shared-react": "workspace:*",
+ "@repo/spike": "workspace:*",
"@tailwindcss/vite": "^4.3.3",
- "@tanstack/react-router": "^1.170.39",
- "@tanstack/react-start": "^1.168.58",
+ "@tanstack/react-router": "^1.170.41",
+ "@tanstack/react-start": "^1.168.60",
"react": "^19.3.0",
"react-dom": "^19.3.0",
"tailwindcss": "^4.3.3",
- "zod": "^4.6.5",
- "@repo/shared-webauthn": "workspace:*"
+ "zod": "^4.6.5"
},
"devDependencies": {
- "@types/node": "^26.6.3",
+ "@types/node": "^26.6.4",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.1",
"typescript": "^6.0.3",
- "vite": "^8.3.1"
+ "vite": "^8.3.2"
}
}
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/auth-rpc.ts b/examples/tanstack-start-react/passkey-otp-memory/src/auth-rpc.ts
index a9935dd..d25ffb3 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/auth-rpc.ts
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/auth-rpc.ts
@@ -1,13 +1,35 @@
import { createServerFn } from "@tanstack/react-start";
-import {
- passkeyAuthenticationCredentialSchema,
- passkeyRegistrationCredentialSchema,
-} from "@repo/shared-webauthn";
+import { analytics } from "@repo/spike/demo";
import { z } from "zod";
import { db } from "./db";
-import { auth, emailOtp } from "./auth";
+import { addEmailOtp, passkey, sessionManager } from "./auth";
import { sessionCookie } from "./session-cookie";
+/** The session behind the request's cookie, null when there is none */
+async function getIdentity() {
+ const token = sessionCookie.get();
+
+ return token === null ? null : sessionManager.get(token);
+}
+
+/** What the browser sends after it created a passkey */
+const registrationCredentialSchema = z.object({
+ response: z.object({
+ clientDataJSON: z.string(),
+ attestationObject: z.string(),
+ }),
+});
+
+/** What the browser sends after it signed in with a passkey */
+const authenticationCredentialSchema = z.object({
+ id: z.string(),
+ response: z.object({
+ clientDataJSON: z.string(),
+ authenticatorData: z.string(),
+ signature: z.string(),
+ }),
+});
+
/**
* Request OTP schema
*/
@@ -19,7 +41,7 @@ export const requestOtpSchema = z.object({
* Verify OTP schema
*/
export const verifyOtpSchema = z.object({
- identifier: z.email(),
+ ticket: z.string(),
otp: z.string().length(6),
});
@@ -30,13 +52,13 @@ export const verifyOtpSchema = z.object({
* application user is provisioned only after the ceremony verifies.
*/
export const startRegistration = createServerFn({ method: "POST" }).handler(
- async () => {
- const result = await auth.strategies.passkeys.createRegistrationOptions();
-
- if (!result.success) return { success: false as const };
-
- return { success: true as const, options: result.data };
- },
+ async () => ({
+ success: true as const,
+ options: await passkey.beginRegistration({
+ handle: crypto.randomUUID(),
+ name: "New user",
+ }),
+ }),
);
/**
@@ -46,15 +68,24 @@ export const startRegistration = createServerFn({ method: "POST" }).handler(
* establishes a session.
*/
export const verifyRegistration = createServerFn({ method: "POST" })
- .validator(z.object({ credential: passkeyRegistrationCredentialSchema }))
+ .validator(z.object({ credential: registrationCredentialSchema }))
.handler(async ({ data }) => {
- const result = await auth.strategies.passkeys.verifyRegistration({
- credential: data.credential,
- });
+ const result = await passkey.finishRegistration(data.credential);
- if (!result.success) return { success: false as const };
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
- sessionCookie.set(result.data.session.token, result.data.session.expiresAt);
+ return { success: false as const };
+ }
+
+ const userId = result.data.userHandle;
+ const { isNew } = await db.users.findOrInsert({ userId }, { userId });
+
+ // A ceremony that was started to add a passkey signs nobody up
+ if (!isNew) return { success: false as const };
+
+ sessionCookie.set(await sessionManager.make({ userId }));
+ analytics.track("sign_up", userId);
return { success: true as const };
});
@@ -67,14 +98,18 @@ export const verifyRegistration = createServerFn({ method: "POST" })
*/
export const startAddPasskey = createServerFn({ method: "POST" }).handler(
async () => {
- const result =
- await auth.strategies.passkeys.createAdditionalRegistrationOptions(
- sessionCookie.get(),
- );
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
- if (!result.success) return { success: false as const };
+ const user = await db.users.get(identity.userId);
- return { success: true as const, options: result.data };
+ return {
+ success: true as const,
+ options: await passkey.beginRegistration({
+ handle: identity.userId,
+ name: user?.email ?? identity.userId,
+ }),
+ };
},
);
@@ -85,14 +120,26 @@ export const startAddPasskey = createServerFn({ method: "POST" }).handler(
* for the current user. No session is established.
*/
export const verifyAddPasskey = createServerFn({ method: "POST" })
- .validator(z.object({ credential: passkeyRegistrationCredentialSchema }))
+ .validator(z.object({ credential: registrationCredentialSchema }))
.handler(async ({ data }) => {
- const result = await auth.strategies.passkeys.verifyAdditionalRegistration(
- sessionCookie.get(),
- { credential: data.credential },
- );
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
+
+ const result = await passkey.finishRegistration(data.credential);
+
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
- if (!result.success) return { success: false as const };
+ return { success: false as const };
+ }
+
+ // The ceremony has to be one this user started. The passkey is already
+ // stored when the answer comes back, so it is removed again.
+ if (result.data.userHandle !== identity.userId) {
+ await db.credentials.delete(result.data.credentialId);
+
+ return { success: false as const };
+ }
return { success: true as const };
});
@@ -104,10 +151,10 @@ export const verifyAddPasskey = createServerFn({ method: "POST" })
*/
export const startAuthentication = createServerFn({
method: "POST",
-}).handler(async () => {
- const result = await auth.strategies.passkeys.createAuthenticationOptions();
- return { success: true as const, options: result.data };
-});
+}).handler(async () => ({
+ success: true as const,
+ options: await passkey.beginAuthentication(),
+}));
/**
* Verify passkey authentication
@@ -116,15 +163,20 @@ export const startAuthentication = createServerFn({
* establishes a session.
*/
export const verifyAuthentication = createServerFn({ method: "POST" })
- .validator(z.object({ credential: passkeyAuthenticationCredentialSchema }))
+ .validator(z.object({ credential: authenticationCredentialSchema }))
.handler(async ({ data }) => {
- const result = await auth.strategies.passkeys.verifyAuthentication({
- credential: data.credential,
- });
+ const result = await passkey.finishAuthentication(data.credential);
- if (!result.success) return { success: false as const };
+ if (!result.success) {
+ console.log("[passkey] refused:", result.error);
- sessionCookie.set(result.data.session.token, result.data.session.expiresAt);
+ return { success: false as const };
+ }
+
+ sessionCookie.set(
+ await sessionManager.make({ userId: result.data.userHandle }),
+ );
+ analytics.track("sign_in", result.data.userHandle);
return { success: true as const };
});
@@ -135,12 +187,12 @@ export const verifyAuthentication = createServerFn({ method: "POST" })
* Returns stored credential metadata for the authenticated user.
*/
export const listPasskeys = createServerFn().handler(async () => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
if (!identity) return { passkeys: [] };
- const passkeys = await db.credentials.list(identity.userId);
+ const passkeys = await db.credentials.where({ userId: identity.userId });
return {
- passkeys: passkeys.map((p) => ({ id: p.credentialId })),
+ passkeys: passkeys.map((p) => ({ id: p.id })),
};
});
@@ -152,32 +204,35 @@ export const listPasskeys = createServerFn().handler(async () => {
export const removePasskey = createServerFn({ method: "POST" })
.validator(z.object({ credentialId: z.string() }))
.handler(async ({ data }) => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
if (!identity) return { success: false as const };
- const passkeys = await db.credentials.list(identity.userId);
+ const passkeys = await db.credentials.where({ userId: identity.userId });
if (passkeys.length <= 1) return { success: false as const };
- const owns = passkeys.some((p) => p.credentialId === data.credentialId);
+ const owns = passkeys.some((p) => p.id === data.credentialId);
if (!owns) return { success: false as const };
- db.credentials.delete(data.credentialId);
+ await db.credentials.delete(data.credentialId);
return { success: true as const };
});
/**
* Server function: Request OTP for adding email
*
- * Requires an active session. Sends OTP to the provided email address.
+ * Requires an active session. Sends OTP to the provided email address and
+ * returns the ticket the client sends back with the OTP.
*/
export const requestOtp = createServerFn({ method: "POST" })
.validator(requestOtpSchema)
.handler(async ({ data }) => {
- const identity = await auth.session.get(sessionCookie.get());
- if (!identity) return { success: false };
+ const identity = await getIdentity();
+ if (!identity) return { success: false as const };
- await emailOtp.request(data.identifier);
- return { success: true };
+ return {
+ success: true as const,
+ ticket: await addEmailOtp.send(data.identifier),
+ };
});
/**
@@ -189,13 +244,16 @@ export const requestOtp = createServerFn({ method: "POST" })
export const addEmail = createServerFn({ method: "POST" })
.validator(verifyOtpSchema)
.handler(async ({ data }) => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
if (!identity) return { success: false };
- const verified = await emailOtp.verify(data.identifier, data.otp);
- if (!verified) return { success: false };
+ const verified = await addEmailOtp.verify(data);
+ if (!verified.success) return { success: false };
+
+ const user = await db.users.update(identity.userId, {
+ email: verified.data.identifier,
+ });
- const user = db.users.setEmail(identity.userId, data.identifier);
if (!user) return { success: false };
return { success: true, viewer: user };
@@ -207,7 +265,9 @@ export const addEmail = createServerFn({ method: "POST" })
* Ends the current session and clears the session cookie.
*/
export const signOut = createServerFn({ method: "POST" }).handler(async () => {
- await auth.session.end(sessionCookie.get());
+ const token = sessionCookie.get();
+
+ if (token !== null) await sessionManager.end(token);
sessionCookie.clear();
});
@@ -218,8 +278,8 @@ export const signOut = createServerFn({ method: "POST" }).handler(async () => {
*/
export const signOutAll = createServerFn({ method: "POST" }).handler(
async () => {
- const identity = await auth.session.get(sessionCookie.get());
- if (identity) db.sessions.deleteAllForUser(identity.userId);
+ const identity = await getIdentity();
+ if (identity) await db.sessions.deleteWhere({ userId: identity.userId });
sessionCookie.clear();
},
);
@@ -230,7 +290,7 @@ export const signOutAll = createServerFn({ method: "POST" }).handler(
* Returns the current user if authenticated, or null otherwise.
*/
export const getViewer = createServerFn().handler(async () => {
- const identity = await auth.session.get(sessionCookie.get());
+ const identity = await getIdentity();
- return identity ? (db.users.get(identity.userId) ?? null) : null;
+ return identity ? db.users.get(identity.userId) : null;
});
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/auth.ts b/examples/tanstack-start-react/passkey-otp-memory/src/auth.ts
index c9b5dae..f026fee 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/auth.ts
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/auth.ts
@@ -1,48 +1,78 @@
-import {
- makeAuth,
- makeOpaqueSession,
- makeOtp,
- makePasskeyEngine,
- makePasskeyStrategy,
-} from "authax";
+import { makeOpaqueSessionManager, makeOTP, makePasskey } from "@repo/spike";
import { db } from "./db";
-const session = makeOpaqueSession({
- storage: db.sessions,
- ttl: 30 * 24 * 60 * 60 * 1000,
+/** How long someone stays signed in, in seconds. The cookie lives as long. */
+export const sessionTtl = 30 * 24 * 60 * 60;
+
+export const sessionManager = makeOpaqueSessionManager<{ userId: string }>({
+ store: async (token, row) => {
+ await db.sessions.insert({
+ id: token,
+ userId: row.userId,
+ expiresAt: row.expiresAt,
+ });
+ },
+ get: async (token) => {
+ const row = await db.sessions.get(token);
+
+ return row ? { userId: row.userId, expiresAt: row.expiresAt } : null;
+ },
+ delete: async (token) => {
+ await db.sessions.delete(token);
+ },
+ ttl: sessionTtl,
});
/**
- * OTP as a pure proof primitive: it verifies address ownership for adding an
- * email, and is never an authentication strategy in this flow.
+ * Confirms an email address for someone who is signed in. Nobody signs in
+ * with it in this example.
*/
-export const emailOtp = makeOtp({
- storage: db.otps,
- delivery: {
- send: async (identifier, otp) => {
- console.log(`[OTP] ${identifier}: ${otp}`);
- },
- },
- ttl: 10 * 60 * 1000,
+export const addEmailOtp = makeOTP({
+ purpose: "add-email",
+ store: async (row) => {
+ await db.otps.insert(row);
+ },
+ take: async (ticket) => db.otps.delete(ticket),
+ send: async ({ to, otp }) => {
+ console.log(`[OTP] Add email, ${to}: ${otp}`);
+ },
+ ttl: 10 * 60,
attempts: 3,
});
-const passkey = makePasskeyEngine({
- storage: db.credentials,
- challenge: { storage: db.challenges, ttl: 5 * 60 * 1000 },
- webAuthn: {
- rpId: "localhost",
- rpName: "Auth Passkey → OTP Demo",
- allowedOrigins: ["http://localhost:3104"],
- },
- displayName: async (context) =>
- context.intent === "add"
- ? (db.users.get(context.userId)?.email ?? context.userId)
- : "New user",
- signUp: async () => db.users.create().userId,
- debug: true,
-});
+export const passkey = makePasskey({
+ rpId: "localhost",
+ rpName: "Auth Passkey → OTP Demo",
+ origins: ["http://localhost:3104"],
+ ttl: 5 * 60,
+ storeChallenge: async (challenge, row) => {
+ await db.challenges.insert({
+ id: challenge,
+ userId: row.handle,
+ expiresAt: row.expiresAt,
+ });
+ },
+ takeChallenge: async (challenge) => {
+ const row = await db.challenges.delete(challenge);
+
+ return row ? { handle: row.userId, expiresAt: row.expiresAt } : null;
+ },
+ storeCredential: async (credentialId, row) => {
+ await db.credentials.insert({
+ id: credentialId,
+ userId: row.handle,
+ publicKey: row.publicKey,
+ counter: row.counter,
+ });
+ },
+ getCredential: async (credentialId) => {
+ const row = await db.credentials.get(credentialId);
-export const auth = makeAuth(session, (kernel) => ({
- passkeys: makePasskeyStrategy(kernel, passkey),
-}));
+ return row
+ ? { handle: row.userId, publicKey: row.publicKey, counter: row.counter }
+ : null;
+ },
+ setCounter: async (credentialId, counter) => {
+ await db.credentials.update(credentialId, { counter });
+ },
+});
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/db.ts b/examples/tanstack-start-react/passkey-otp-memory/src/db.ts
index 4b4a627..fc77b7c 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/db.ts
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/db.ts
@@ -4,100 +4,39 @@
* Simple in-memory stores for demonstration purposes. In a real app these
* would be replaced with database queries.
*/
-import type {
- ChallengeRecord,
- CredentialRecord,
- OtpRecord,
- SessionRecord,
-} from "authax";
-
-const users = new Map();
-let userIdCounter = 0;
-
-const sessions = new Map();
-const otps = new Map();
-const credentials = new Map();
-const challenges = new Map();
+import { makeMemoryTable } from "@repo/spike/demo";
export const db = {
- users: {
- create: () => {
- const userId = `user_${++userIdCounter}`;
- users.set(userId, { userId });
- return { userId };
- },
-
- get: (userId: string) => users.get(userId),
-
- setEmail: (userId: string, email: string) => {
- const user = users.get(userId);
- if (!user) return undefined;
- user.email = email;
- return user;
- },
- },
-
- sessions: {
- store: async (record: SessionRecord) => {
- sessions.set(record.sessionId, record);
- },
-
- get: async (sessionId: string) => sessions.get(sessionId) ?? null,
-
- delete: async (sessionId: string) => {
- sessions.delete(sessionId);
- },
-
- deleteAllForUser: (userId: string) => {
- for (const [sessionId, record] of sessions) {
- if (record.userId === userId) {
- sessions.delete(sessionId);
- }
- }
- },
- },
-
- otps: {
- store: async (record: OtpRecord) => {
- otps.set(record.identifier, record);
- },
-
- take: async (identifier: string) => {
- const record = otps.get(identifier) ?? null;
- otps.delete(identifier);
- return record;
- },
- },
-
- credentials: {
- store: async (record: CredentialRecord) => {
- credentials.set(record.credentialId, record);
- },
-
- get: async (credentialId: string) => credentials.get(credentialId) ?? null,
-
- list: async (userId: string) =>
- Array.from(credentials.values()).filter((c) => c.userId === userId),
-
- setCounter: async (credentialId: string, counter: number) => {
- const record = credentials.get(credentialId);
- if (record) credentials.set(credentialId, { ...record, counter });
- },
-
- delete: (credentialId: string) => {
- credentials.delete(credentialId);
- },
- },
-
- challenges: {
- store: async (record: ChallengeRecord) => {
- challenges.set(record.challenge, record);
- },
-
- take: async (challenge: string) => {
- const record = challenges.get(challenge) ?? null;
- challenges.delete(challenge);
- return record;
- },
- },
+ users: makeMemoryTable<{
+ userId: string;
+ email?: string;
+ }>("userId"),
+
+ sessions: makeMemoryTable<{
+ id: string;
+ userId: string;
+ expiresAt: number;
+ }>("id"),
+
+ otps: makeMemoryTable<{
+ ticket: string;
+ identifier: string;
+ otp: string;
+ expiresAt: number;
+ attemptsLeft: number;
+ }>("ticket"),
+
+ credentials: makeMemoryTable<{
+ id: string;
+ userId: string;
+ publicKey: string;
+ counter: number;
+ }>("id"),
+
+ challenges: makeMemoryTable<{
+ id: string;
+ /** Who a registration is for. Null when the challenge is for signing in. */
+ userId: string | null;
+ expiresAt: number;
+ }>("id"),
};
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/routes/index.tsx b/examples/tanstack-start-react/passkey-otp-memory/src/routes/index.tsx
index 15c5ea7..a36b792 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/routes/index.tsx
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/routes/index.tsx
@@ -86,6 +86,7 @@ function UnauthenticatedView(props: { onSignedIn: () => void }) {
function AddEmailFlow(props: { onSuccess: () => void }) {
const [step, setStep] = useState<"email" | "otp">("email");
const [email, setEmail] = useState("");
+ const [ticket, setTicket] = useState("");
const [otp, setOtp] = useState("");
const [error, setError] = useState(null);
@@ -97,6 +98,7 @@ function AddEmailFlow(props: { onSuccess: () => void }) {
e.preventDefault();
const result = await requestOtp({ data: { identifier: email } });
if (result.success) {
+ setTicket(result.ticket);
setStep("otp");
setError(null);
} else {
@@ -125,7 +127,7 @@ function AddEmailFlow(props: { onSuccess: () => void }) {
onSubmit={async (e) => {
e.preventDefault();
const result = await addEmail({
- data: { identifier: email, otp },
+ data: { ticket, otp },
});
if (result.success) {
props.onSuccess();
@@ -141,9 +143,7 @@ function AddEmailFlow(props: { onSuccess: () => void }) {
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/session-cookie.ts b/examples/tanstack-start-react/passkey-otp-memory/src/session-cookie.ts
index 60eb30c..4f99d5a 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/session-cookie.ts
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/session-cookie.ts
@@ -1,4 +1,5 @@
import { getCookie, setCookie } from "@tanstack/react-start/server";
+import { sessionTtl } from "./auth";
const name = "session";
@@ -12,7 +13,7 @@ const options = {
/** Moves the session token between the request and the auth API */
export const sessionCookie = {
get: () => getCookie(name) ?? null,
- set: (token: string, expiresAt: Date) =>
- setCookie(name, token, { ...options, expires: expiresAt }),
+ set: (token: string) =>
+ setCookie(name, token, { ...options, maxAge: sessionTtl }),
clear: () => setCookie(name, "", { ...options, maxAge: 0 }),
};
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/styles.css b/examples/tanstack-start-react/passkey-otp-memory/src/styles.css
index 017b778..3306d99 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/styles.css
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/styles.css
@@ -1,10 +1,2 @@
@import "tailwindcss";
@import "@repo/shared-react/styles.css";
-
-/* Button cursor */
-@layer base {
- button:not(:disabled),
- [role="button"]:not(:disabled) {
- cursor: pointer;
- }
-}
diff --git a/examples/tanstack-start-react/passkey-otp-memory/src/use-passkey.ts b/examples/tanstack-start-react/passkey-otp-memory/src/use-passkey.ts
index dbb95f6..a8659b1 100644
--- a/examples/tanstack-start-react/passkey-otp-memory/src/use-passkey.ts
+++ b/examples/tanstack-start-react/passkey-otp-memory/src/use-passkey.ts
@@ -82,7 +82,7 @@ export function usePasskeyAuthentication(
const start = await options.start();
if (!start.success) {
- setError("Failed to start sign-in");
+ setError("Failed to start authentication");
setLoading(false);
return;
}
@@ -90,7 +90,7 @@ export function usePasskeyAuthentication(
const credential = await getPasskey(start.options);
if (!credential) {
- setError("Passkey sign-in was cancelled");
+ setError("Passkey authentication was cancelled");
setLoading(false);
return;
}
@@ -100,11 +100,11 @@ export function usePasskeyAuthentication(
if (result.success) {
options.onSuccess();
} else {
- setError("Failed to sign in");
+ setError("Failed to authenticate with passkey");
}
} catch (cause) {
- console.error("Passkey sign-in failed:", cause);
- setError("Passkey sign-in failed");
+ console.error("Passkey authentication failed:", cause);
+ setError("Passkey authentication failed");
}
setLoading(false);
diff --git a/package.json b/package.json
index 09abcb3..32b70f6 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"name": "@repo/monorepo",
"private": true,
"type": "module",
+ "packageManager": "bun@1.4.2",
"workspaces": [
"packages/*",
"examples/*/*"
@@ -14,7 +15,7 @@
"test:run": "vitest run",
"test:watch": "vitest",
"typecheck": "bun run --filter '*' typecheck",
- "check": "bun run format:check && bun run lint:check && bun run test:run && bun run typecheck",
+ "check": "bun install --frozen-lockfile && bun run format:check && bun run lint:check && bun run test:run && bun run typecheck",
"outdated": "bun outdated --recursive",
"update": "bun update --recursive",
"update:latest": "bun update --latest --recursive",
@@ -25,6 +26,6 @@
"prettier": "^3.9.9",
"prettier-plugin-tailwindcss": "^0.8.1",
"typescript": "^6.0.3",
- "vitest": "^5.0.2"
+ "vitest": "^5.0.3"
}
}
diff --git a/packages/authax/package.json b/packages/authax/package.json
index 1f56c21..c6b1f3e 100644
--- a/packages/authax/package.json
+++ b/packages/authax/package.json
@@ -21,7 +21,7 @@
},
"peerDependencies": {
"@tanstack/react-start": "^1.161.1",
- "next": "^16.3.6"
+ "next": "^16.3.8"
},
"peerDependenciesMeta": {
"@tanstack/react-start": {
@@ -33,9 +33,9 @@
},
"devDependencies": {
"@eslint/js": "^10.0.1",
- "@tanstack/react-start": "^1.168.58",
- "eslint": "^10.11.0",
- "neon-testing": "^3.0.1",
- "typescript-eslint": "^8.70.1"
+ "@tanstack/react-start": "^1.168.60",
+ "eslint": "^10.12.0",
+ "neon-testing": "^3.0.2",
+ "typescript-eslint": "^8.71.0"
}
}
diff --git a/packages/spike/README.md b/packages/spike/README.md
new file mode 100644
index 0000000..bcfda14
--- /dev/null
+++ b/packages/spike/README.md
@@ -0,0 +1,44 @@
+# Spike
+
+This library consists of three main entities.
+
+- Session managers
+- Authentication strategies
+- Proofs
+
+This is how they relate.
+
+- A strategy authenticates and issues a proof of what was proven.
+- A session manager consumes the proof and issues a session.
+- The app hands the proof from one to the other and decides what the session holds.
+
+Strategies and session managers are completely independent. They communicate using proofs.
+
+## Decisions
+
+- An OTP is verified against the request that asked for it, not the identifier. The requesting client holds a random ticket from `send`, and the OTP alone is useless without it. Signing in on another device is a magic link's job.
+- The library generates every string it hands a client. OTP tickets, passkey challenges, and session tokens are random columns the library fills, never the app's ids. The app keys its tables however it likes.
+- A different trade-off is a different factory. An OTP verified against the identifier, with nothing for the client to hold, would sit beside the ticket one rather than change it.
+- The library is not extensible. It ships the strategies and session managers, and an app wires them and writes none of its own. `issueProof` and `consumeProof` are not exported, so an agent cannot shortcut sign-in through the library and have it look like proper use. Auth is what everyone gets wrong, agents most of all, so the scrutiny belongs in one place. A new strategy or session manager is a pull request.
+
+## OTP lookup options
+
+The identifier is the email address or phone number the OTP is sent to.
+
+| | Ticket | Identifier, one pending | Identifier, many pending |
+| --- | --- | --- | --- |
+| Verify looks up by | the ticket | the identifier | the identifier and the OTP together |
+| Client holds between send and verify | the ticket | nothing | nothing |
+| Typing the OTP on another device | does not work | works | works |
+| A second request for the same identifier | stands beside the first | replaces the first | stands beside the first |
+| A guess is tested against | one OTP | one OTP | every pending OTP for that identifier |
+| Wrong attempts are counted on | the request row | the identifier row | a separate counter per identifier |
+| Someone sees the OTP mid sign-in | useless without the ticket | usable with the identifier | usable with the identifier |
+| A stranger requests an OTP for your identifier | yours is untouched | yours stops working | one more OTP that would work |
+
+Ticket is the only option where an OTP is bound to the client that asked and where a stranger's request changes nothing for you. Identifier with one pending is the simplest for the app and the weakest against interference. Identifier with many pending has the costs of both and should not be built.
+
+## Revisit
+
+- Opening the library to bespoke strategies and session managers, by exporting `issueProof` and `consumeProof` or a `makeStrategy` factory around them. Either hands the shortcut back, so only on real demand.
+- Type contracts. For now the types are inferred from the functions, which shows what is needed and what is not. Once the shapes settle, a new strategy or session manager starts from its contract type, as in `const makeThing: Contract = (args) => {}`.
diff --git a/packages/spike/package.json b/packages/spike/package.json
new file mode 100644
index 0000000..7e517b0
--- /dev/null
+++ b/packages/spike/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@repo/spike",
+ "private": true,
+ "type": "module",
+ "exports": {
+ ".": "./src/index.ts",
+ "./demo": "./src/demo/index.ts"
+ },
+ "scripts": {
+ "typecheck": "tsc"
+ }
+}
diff --git a/packages/spike/spike-authenticator.ts b/packages/spike/spike-authenticator.ts
new file mode 100644
index 0000000..0def0af
--- /dev/null
+++ b/packages/spike/spike-authenticator.ts
@@ -0,0 +1,161 @@
+import { base64urlEncode, sha256 } from "./src/webauthn/crypto";
+
+/**
+ * A software authenticator for the playground. It stands in for the browser
+ * and the device, and answers ceremony options the way they would. The first
+ * passkey it made is the one it signs in with.
+ */
+export function makeAuthenticator(origin: string) {
+ const passkeys: {
+ credentialId: Uint8Array;
+ privateKey: CryptoKey;
+ uses: number;
+ }[] = [];
+
+ return {
+ /** What navigator.credentials.create returns, as JSON */
+ create: async (options: PublicKeyCredentialCreationOptionsJSON) => {
+ const keys = await crypto.subtle.generateKey(
+ { name: "ECDSA", namedCurve: "P-256" },
+ true,
+ ["sign", "verify"],
+ );
+ const jwk = await crypto.subtle.exportKey("jwk", keys.publicKey);
+ const credentialId = crypto.getRandomValues(new Uint8Array(16));
+
+ passkeys.push({ credentialId, privateKey: keys.privateKey, uses: 0 });
+
+ const authenticatorData = [
+ ...(await authenticatorDataHead(options.rp.id ?? "", 0x45, 0)),
+ ...new Uint8Array(16),
+ credentialId.length >> 8,
+ credentialId.length & 0xff,
+ ...credentialId,
+ ...map([
+ [integer(1), integer(2)],
+ [integer(3), integer(-7)],
+ [integer(-1), integer(1)],
+ [integer(-2), bytes(decode(jwk.x ?? ""))],
+ [integer(-3), bytes(decode(jwk.y ?? ""))],
+ ]),
+ ];
+
+ return {
+ response: {
+ clientDataJSON: clientData("webauthn.create", options.challenge),
+ attestationObject: base64urlEncode(
+ new Uint8Array(
+ map([
+ [text("fmt"), text("none")],
+ [text("attStmt"), map([])],
+ [text("authData"), bytes(new Uint8Array(authenticatorData))],
+ ]),
+ ),
+ ),
+ },
+ };
+ },
+
+ /** What navigator.credentials.get returns, as JSON */
+ get: async (options: PublicKeyCredentialRequestOptionsJSON) => {
+ const [passkey] = passkeys;
+
+ if (passkey === undefined) throw new Error("no passkey on this device");
+
+ passkey.uses += 1;
+
+ const clientDataJSON = clientData("webauthn.get", options.challenge);
+ const authenticatorData = new Uint8Array(
+ await authenticatorDataHead(options.rpId ?? "", 0x05, passkey.uses),
+ );
+ const signature = await crypto.subtle.sign(
+ { name: "ECDSA", hash: "SHA-256" },
+ passkey.privateKey,
+ new Uint8Array([
+ ...authenticatorData,
+ ...(await sha256(decode(clientDataJSON))),
+ ]),
+ );
+
+ return {
+ id: base64urlEncode(passkey.credentialId),
+ response: {
+ clientDataJSON,
+ authenticatorData: base64urlEncode(authenticatorData),
+ signature: base64urlEncode(der(new Uint8Array(signature))),
+ },
+ };
+ },
+ };
+
+ function clientData(type: string, challenge: string) {
+ return base64urlEncode(
+ JSON.stringify({ type, challenge, origin, crossOrigin: false }),
+ );
+ }
+}
+
+/** The hash of the relying party id, the flags, and the counter */
+async function authenticatorDataHead(
+ rpId: string,
+ flags: number,
+ counter: number,
+) {
+ return [
+ ...(await sha256(new TextEncoder().encode(rpId))),
+ flags,
+ counter >>> 24,
+ (counter >> 16) & 0xff,
+ (counter >> 8) & 0xff,
+ counter & 0xff,
+ ];
+}
+
+function decode(base64url: string) {
+ const binary = atob(base64url.replaceAll("-", "+").replaceAll("_", "/"));
+
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
+}
+
+/** Web Crypto signs as r and s side by side, WebAuthn sends them as DER */
+function der(signature: Uint8Array) {
+ const part = (value: Uint8Array) => {
+ const trimmed = [...value];
+
+ while (trimmed.length > 1 && trimmed[0] === 0) trimmed.shift();
+
+ const positive = (trimmed[0] ?? 0) & 0x80 ? [0, ...trimmed] : trimmed;
+
+ return [0x02, positive.length, ...positive];
+ };
+
+ const r = part(signature.subarray(0, 32));
+ const s = part(signature.subarray(32));
+
+ return new Uint8Array([0x30, r.length + s.length, ...r, ...s]);
+}
+
+// The few CBOR encoders the two responses need
+
+function head(major: number, length: number) {
+ if (length < 24) return [(major << 5) | length];
+ if (length < 256) return [(major << 5) | 24, length];
+
+ return [(major << 5) | 25, length >> 8, length & 0xff];
+}
+
+function integer(value: number) {
+ return value >= 0 ? head(0, value) : head(1, -1 - value);
+}
+
+function bytes(value: Uint8Array) {
+ return [...head(2, value.length), ...value];
+}
+
+function text(value: string) {
+ return [...head(3, value.length), ...new TextEncoder().encode(value)];
+}
+
+function map(entries: [number[], number[]][]) {
+ return [...head(5, entries.length), ...entries.flat(2)];
+}
diff --git a/packages/spike/spike-no-kernel.ts b/packages/spike/spike-no-kernel.ts
new file mode 100644
index 0000000..3de4ad9
--- /dev/null
+++ b/packages/spike/spike-no-kernel.ts
@@ -0,0 +1,231 @@
+import {
+ makeOpaqueSessionManager,
+ makeOTP,
+ makePasskey,
+ makeSignedSessionManager,
+} from "./src/index";
+import { makeMemoryTable } from "./src/demo/index";
+import { makeAuthenticator } from "./spike-authenticator";
+
+/**
+ * App
+ */
+
+const sessionsTable = makeMemoryTable<{
+ token: string;
+ userId: string;
+ expiresAt: number;
+}>("token");
+const otpsTable = makeMemoryTable<{
+ ticket: string;
+ identifier: string;
+ otp: string;
+ expiresAt: number;
+ attemptsLeft: number;
+}>("ticket");
+const challengesTable = makeMemoryTable<{
+ challenge: string;
+ handle: string | null;
+ expiresAt: number;
+}>("challenge");
+const credentialsTable = makeMemoryTable<{
+ credentialId: string;
+ handle: string;
+ publicKey: string;
+ counter: number;
+}>("credentialId");
+
+// Stands in for the browser and the device
+const authenticator = makeAuthenticator("http://localhost:3000");
+
+// Captures what would have been delivered, for the demo
+const delivered = new Map();
+
+const opaque = makeOpaqueSessionManager<{ userId: string }>({
+ store: async (token, row) => {
+ await sessionsTable.insert({ token, ...row });
+ },
+ get: (token) => sessionsTable.get(token),
+ delete: async (token) => {
+ await sessionsTable.delete(token);
+ },
+ ttl: 30 * 24 * 60 * 60,
+});
+
+const emailOtp = makeOTP({
+ purpose: "sign-in-email",
+ store: async (row) => {
+ await otpsTable.insert(row);
+ },
+ take: (ticket) => otpsTable.delete(ticket),
+ send: async ({ to, otp }) => {
+ console.log("email to:", to, "otp:", otp);
+ delivered.set(to, otp);
+ },
+ ttl: 10 * 60,
+ attempts: 3,
+});
+
+const smsOtp = makeOTP({
+ purpose: "sign-in-sms",
+ store: async (row) => {
+ await otpsTable.insert(row);
+ },
+ take: (ticket) => otpsTable.delete(ticket),
+ send: async ({ to, otp }) => {
+ console.log("sms to:", to, "otp:", otp);
+ delivered.set(to, otp);
+ },
+ ttl: 10 * 60,
+ attempts: 3,
+});
+
+const passkey = makePasskey({
+ rpId: "localhost",
+ rpName: "Spike",
+ origins: ["http://localhost:3000"],
+ ttl: 5 * 60,
+ storeChallenge: async (challenge, row) => {
+ await challengesTable.insert({ challenge, ...row });
+ },
+ takeChallenge: (challenge) => challengesTable.delete(challenge),
+ storeCredential: async (credentialId, row) => {
+ await credentialsTable.insert({ credentialId, ...row });
+ },
+ getCredential: (credentialId) => credentialsTable.get(credentialId),
+ setCounter: async (credentialId, counter) => {
+ await credentialsTable.update(credentialId, { counter });
+ },
+});
+
+//
+// Playground
+//
+
+console.log("-".repeat(80));
+
+// Email. 1. request, 2. verify, 3. session
+const emailTicket = await emailOtp.send("ripley@example.com");
+
+const emailProof = await emailOtp.verify({
+ ticket: emailTicket,
+ otp: delivered.get("ripley@example.com") ?? "",
+});
+if (!emailProof.success) throw new Error(emailProof.error);
+
+const emailSessionId = await opaque.make({
+ userId: `user-for-${emailProof.data.identifier}`,
+});
+console.log("SESSION", await opaque.get(emailSessionId));
+
+// SMS. Same three steps, other instance
+const smsTicket = await smsOtp.send("+15555550100");
+
+const smsProof = await smsOtp.verify({
+ ticket: smsTicket,
+ otp: delivered.get("+15555550100") ?? "",
+});
+if (!smsProof.success) throw new Error(smsProof.error);
+
+const smsSessionId = await opaque.make({
+ userId: `user-for-${smsProof.data.identifier}`,
+});
+console.log("SESSION", await opaque.get(smsSessionId));
+
+// Passkey sign-up. 1. app creates the user, 2. begin with its id as the
+// handle, 3. browser creates a credential, 4. finish, 5. session
+const ripley = "user-ripley";
+const signUp = await passkey.beginRegistration({
+ handle: ripley,
+ name: "ripley@example.com",
+});
+console.log("OPTIONS", signUp);
+const registered = await passkey.finishRegistration(
+ await authenticator.create(signUp),
+);
+if (!registered.success) throw new Error(registered.error);
+
+const signUpSessionId = await opaque.make({
+ userId: registered.data.userHandle,
+});
+console.log("SESSION", await opaque.get(signUpSessionId));
+
+// Passkey sign-in. 1. begin, 2. browser signs, 3. finish, 4. session
+const signIn = await passkey.beginAuthentication();
+const authenticated = await passkey.finishAuthentication(
+ await authenticator.get(signIn),
+);
+if (!authenticated.success) throw new Error(authenticated.error);
+
+const signInSessionId = await opaque.make({
+ userId: authenticated.data.userHandle,
+});
+console.log("SESSION", await opaque.get(signInSessionId));
+
+// Add a passkey while signed in. The session says who, its user id is the
+// handle, no new session
+const current = await opaque.get(signInSessionId);
+if (current === null) throw new Error("not signed in");
+
+const add = await passkey.beginRegistration({
+ handle: current.userId,
+ name: "ripley@example.com",
+});
+const added = await passkey.finishRegistration(await authenticator.create(add));
+if (!added.success) throw new Error(added.error);
+console.log("ADDED", added.data);
+
+// Signed session manager. Same three steps, no table. The token carries the
+// session
+const signed = makeSignedSessionManager<{ userId: string }>({
+ secret: "spike-secret",
+ ttl: 60 * 60,
+});
+
+const signedTicket = await emailOtp.send("ripley@example.com");
+const signedProof = await emailOtp.verify({
+ ticket: signedTicket,
+ otp: delivered.get("ripley@example.com") ?? "",
+});
+if (!signedProof.success) throw new Error(signedProof.error);
+
+const token = await signed.make({
+ userId: `user-for-${signedProof.data.identifier}`,
+});
+console.log("TOKEN", token);
+console.log("SIGNED SESSION", await signed.get(token));
+console.log("TAMPERED", await signed.get(`${token.slice(0, -2)}xx`));
+
+// Failures come back with a reason
+const wrongTicket = await emailOtp.send("ripley@example.com");
+console.log(
+ "WRONG",
+ await emailOtp.verify({ ticket: wrongTicket, otp: "nope" }),
+);
+console.log(
+ "USED",
+ await emailOtp.verify({ ticket: emailTicket, otp: "nope" }),
+);
+
+// A device with a passkey the app never stored
+const stranger = makeAuthenticator("http://localhost:3000");
+await stranger.create(
+ await passkey.beginRegistration({ handle: "nobody", name: "nobody" }),
+);
+console.log(
+ "STRANGER",
+ await passkey.finishAuthentication(
+ await stranger.get(await passkey.beginAuthentication()),
+ ),
+);
+
+// The right passkey, presented from another site
+const elsewhere = makeAuthenticator("http://evil.example");
+console.log(
+ "ELSEWHERE",
+ await passkey.finishRegistration(
+ await elsewhere.create(
+ await passkey.beginRegistration({ handle: ripley, name: "ripley" }),
+ ),
+ ),
+);
diff --git a/packages/spike/src/demo/analytics.ts b/packages/spike/src/demo/analytics.ts
new file mode 100644
index 0000000..fa2c764
--- /dev/null
+++ b/packages/spike/src/demo/analytics.ts
@@ -0,0 +1,6 @@
+/** For demos only. Stands in for an analytics service and only logs. */
+export const analytics = {
+ track: (event: "sign_up" | "sign_in", userId: string) => {
+ console.log(`[analytics] ${event}: ${userId}`);
+ },
+};
diff --git a/packages/spike/src/demo/index.ts b/packages/spike/src/demo/index.ts
new file mode 100644
index 0000000..267a4f9
--- /dev/null
+++ b/packages/spike/src/demo/index.ts
@@ -0,0 +1,2 @@
+export { analytics } from "./analytics";
+export { makeMemoryTable } from "./memory-table";
diff --git a/packages/spike/src/demo/memory-table.ts b/packages/spike/src/demo/memory-table.ts
new file mode 100644
index 0000000..cde00f4
--- /dev/null
+++ b/packages/spike/src/demo/memory-table.ts
@@ -0,0 +1,93 @@
+/** The fields of a row that hold a string, the only ones that can be its key */
+type StringField = {
+ [Field in keyof Row]: Row[Field] extends string ? Field : never;
+}[keyof Row];
+
+/**
+ * For demos only. Rows live in memory and are lost on restart. Every function
+ * is async, so the code that calls it reads as it would against a database.
+ * key names the field that holds the key of a row.
+ */
+export function makeMemoryTable(
+ key: StringField & keyof Row,
+) {
+ const rows = new Map();
+
+ const insert = (row: Row) => {
+ const id = String(row[key]);
+
+ if (rows.has(id)) throw new Error("a row with this key already exists");
+
+ rows.set(id, { ...row });
+ };
+
+ const where = (match: Partial) =>
+ [...rows.values()]
+ .filter((row) =>
+ Object.entries(match).every(
+ ([field, value]) => row[field as keyof Row] === value,
+ ),
+ )
+ .map((row) => ({ ...row }));
+
+ return {
+ /** Stores a new row under its key. Throws when the key is taken. */
+ insert: async (row: Row) => insert(row),
+
+ /** The row for a key, null when there is none */
+ get: async (id: string) => {
+ const row = rows.get(id);
+
+ return row === undefined ? null : { ...row };
+ },
+
+ /**
+ * Changes fields of the row for a key and returns the row, null when
+ * there is none. The key itself cannot change.
+ */
+ update: async (id: string, fields: Partial) => {
+ const row = rows.get(id);
+
+ if (row === undefined) return null;
+ if (key in fields) throw new Error("the key of a row cannot change");
+
+ rows.set(id, { ...row, ...fields });
+
+ return { ...row, ...fields };
+ },
+
+ /** Removes the row for a key and returns it, null when there is none */
+ delete: async (id: string) => {
+ const row = rows.get(id) ?? null;
+ rows.delete(id);
+
+ return row;
+ },
+
+ /** Removes every row whose fields equal the ones given and returns them */
+ deleteWhere: async (match: Partial) => {
+ const removed = where(match);
+
+ for (const row of removed) rows.delete(String(row[key]));
+
+ return removed;
+ },
+
+ /** Every row whose fields equal the ones given */
+ where: async (match: Partial) => where(match),
+
+ /**
+ * The first row whose fields equal the ones in match. When there is none,
+ * row is inserted and returned. isNew tells which of the two happened.
+ */
+ findOrInsert: async (match: Partial, row: Row) => {
+ const [found] = where(match);
+
+ if (found !== undefined) return { row: found, isNew: false };
+
+ insert(row);
+
+ return { row: { ...row }, isNew: true };
+ },
+ };
+}
diff --git a/packages/spike/src/flows/otp-auth-flow.ts b/packages/spike/src/flows/otp-auth-flow.ts
new file mode 100644
index 0000000..d3e1c6c
--- /dev/null
+++ b/packages/spike/src/flows/otp-auth-flow.ts
@@ -0,0 +1,152 @@
+import { makeOTP, type OtpRow } from "../strategies/otp";
+import { routeNames } from "../transports/route-names";
+import { isRecord } from "../webauthn/lib";
+
+/** What a handler answers. A transport turns it into a response. */
+export type HandlerResult = {
+ status: number;
+ /** Sent to the browser as JSON */
+ body: Body;
+ /** A new session token for the cookie. Null leaves the cookie alone. */
+ setSession: string | null;
+};
+
+/** What the otp-send route answers */
+export type OtpSendBody =
+ { success: true; ticket: string } | { success: false };
+
+/** What the otp-verify route answers */
+export type OtpVerifyBody =
+ { success: true; isNew: boolean } | { success: false; error: string };
+
+/** Ten minutes and three guesses, to spread into makeOtpAuthFlow */
+export const recommendedOtpConfig = { otpTtl: 10 * 60, otpAttempts: 3 };
+
+/**
+ * Sign in or sign up by OTP, in two calls. otp-send sends an OTP and returns
+ * the ticket. otp-verify checks the OTP, resolves the user, and makes the
+ * session, in that order. A ticket made by an OTP with another purpose is
+ * refused.
+ */
+export function makeOtpAuthFlow(args: {
+ /**
+ * Stores a new OTP row. Also called after a wrong guess, to put the row
+ * back with one attempt less.
+ */
+ storeOtp: (row: OtpRow) => Promise;
+ /**
+ * Removes the row for a ticket and returns it, in one step. Null when there
+ * is none. It must remove the row. A row that stays lets the same OTP be
+ * used again until it expires.
+ */
+ takeOtp: (ticket: string) => Promise;
+ /** Delivers the OTP to an email address or a phone number */
+ sendOtp: (message: { to: string; otp: string }) => Promise;
+ /** Lifetime of an OTP in seconds */
+ otpTtl: number;
+ /** How many guesses one ticket allows */
+ otpAttempts: number;
+ sessionManager: { make: (session: { userId: string }) => Promise };
+ /**
+ * Turns the verified email address or phone number into the user to sign
+ * in. Find the user, create one, or refuse, as the app decides. A refusal
+ * comes after the OTP is checked, so only the owner of the address learns
+ * the reason.
+ */
+ resolveUser: (
+ identifier: string,
+ ) => Promise<
+ | { success: true; user: User; isNew: boolean }
+ | { success: false; error: string }
+ >;
+ /** A hook must not throw. What happens when one does is not decided. */
+ hooks?: {
+ /** Runs once the OTP is sent */
+ onSend?: (sent: { identifier: string }) => Promise | void;
+ /** Runs once the session is made, such as for analytics */
+ onSignIn?: (signedIn: {
+ user: User;
+ isNew: boolean;
+ }) => Promise | void;
+ };
+}) {
+ const otp = makeOTP({
+ purpose: "otp-auth-flow",
+ store: args.storeOtp,
+ take: args.takeOtp,
+ send: args.sendOtp,
+ ttl: args.otpTtl,
+ attempts: args.otpAttempts,
+ });
+
+ const send = async (input: unknown): Promise> => {
+ if (!isRecord(input) || typeof input.identifier !== "string") {
+ return { status: 400, body: { success: false }, setSession: null };
+ }
+
+ const ticket = await otp.send(input.identifier);
+
+ await args.hooks?.onSend?.({ identifier: input.identifier });
+
+ return { status: 200, body: { success: true, ticket }, setSession: null };
+ };
+
+ const verify = async (
+ input: unknown,
+ ): Promise> => {
+ if (
+ !isRecord(input) ||
+ typeof input.ticket !== "string" ||
+ typeof input.otp !== "string"
+ ) {
+ return {
+ status: 400,
+ body: { success: false, error: "invalid_input" },
+ setSession: null,
+ };
+ }
+
+ const verified = await otp.verify({
+ ticket: input.ticket,
+ otp: input.otp,
+ });
+
+ if (!verified.success) {
+ return {
+ status: 200,
+ body: { success: false, error: verified.error },
+ setSession: null,
+ };
+ }
+
+ const resolved = await args.resolveUser(verified.data.identifier);
+
+ if (!resolved.success) {
+ return {
+ status: 200,
+ body: { success: false, error: resolved.error },
+ setSession: null,
+ };
+ }
+
+ const token = await args.sessionManager.make({
+ userId: resolved.user.userId,
+ });
+
+ await args.hooks?.onSignIn?.({
+ user: resolved.user,
+ isNew: resolved.isNew,
+ });
+
+ return {
+ status: 200,
+ body: { success: true, isNew: resolved.isNew },
+ setSession: token,
+ };
+ };
+
+ return {
+ /** Mount these with serve, under the names the client calls */
+ routes: { [routeNames.otpSend]: send, [routeNames.otpVerify]: verify },
+ };
+}
diff --git a/packages/spike/src/index.ts b/packages/spike/src/index.ts
new file mode 100644
index 0000000..8689036
--- /dev/null
+++ b/packages/spike/src/index.ts
@@ -0,0 +1,7 @@
+export { makeOtpAuthFlow, recommendedOtpConfig } from "./flows/otp-auth-flow";
+export { makeOpaqueSessionManager } from "./sessions/opaque";
+export { makeSignedSessionManager } from "./sessions/signed";
+export { makeOTP } from "./strategies/otp";
+export { makePasskey } from "./strategies/passkey";
+export { serve } from "./transports/rest";
+export { makeAuthClient } from "./transports/rest-client";
diff --git a/packages/spike/src/index.typecheck.ts b/packages/spike/src/index.typecheck.ts
new file mode 100644
index 0000000..39fc0aa
--- /dev/null
+++ b/packages/spike/src/index.typecheck.ts
@@ -0,0 +1,19 @@
+/**
+ * The export guards written during the spike, kept so they are not lost.
+ * Typechecked, never run. Each expected error proves a name is absent from
+ * the public entry.
+ */
+
+// @ts-expect-error only the library issues proofs
+import { issueProof } from "./index";
+// @ts-expect-error only the library consumes proofs
+import { consumeProof } from "./index";
+// @ts-expect-error failures are made by strategies, the app only reads them
+import { fail } from "./index";
+// @ts-expect-error an app never has to name a proof
+import type { Proof } from "./index";
+// @ts-expect-error an app never has to name a failure
+import type { Failure } from "./index";
+
+export const absent = [issueProof, consumeProof, fail];
+export type Absent = [Proof