fastify-siwe-middleware 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # fastify-siwe-middleware
2
+
3
+ Fastify middleware helpers for SIWE-based auth flows.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install fastify-siwe-middleware
9
+ ```
10
+
11
+ ## Public API
12
+
13
+ The package exports three middleware factories from the package root:
14
+
15
+ - `jwtGuard`
16
+ - `onChainGuard`
17
+ - `roleGuard`
18
+
19
+ It also exports the shared auth types:
20
+
21
+ - `AuthUser`
22
+ - `AuthJwtPayload`
23
+
24
+ ## Usage
25
+
26
+ ```ts
27
+ import fastify from "fastify";
28
+ import {
29
+ jwtGuard,
30
+ onChainGuard,
31
+ roleGuard,
32
+ } from "fastify-siwe-middleware";
33
+
34
+ const app = fastify();
35
+
36
+ const authGuard = jwtGuard({
37
+ secret: process.env.JWT_SECRET!,
38
+ isSessionActive: async (sessionId) => {
39
+ return sessionId.length > 0;
40
+ },
41
+ });
42
+
43
+ app.get("/private", { preHandler: [authGuard] }, async (req) => {
44
+ return { address: req.user.address };
45
+ });
46
+
47
+ app.get(
48
+ "/token-gated",
49
+ {
50
+ preHandler: [
51
+ authGuard,
52
+ onChainGuard({
53
+ rpcUrl: process.env.RPC_URL!,
54
+ contractAddress: "0x0000000000000000000000000000000000000000",
55
+ minBalance: 1n,
56
+ }),
57
+ roleGuard({
58
+ allowlist: ["0x1234567890abcdef1234567890abcdef12345678"],
59
+ }),
60
+ ],
61
+ },
62
+ async () => {
63
+ return { ok: true };
64
+ },
65
+ );
66
+ ```
67
+
68
+ ## Served routes
69
+
70
+ When running this application as an auth service, it exposes the routes below.
71
+
72
+ ### `GET /docs`
73
+
74
+ - Auth: none
75
+ - Description: Interactive Swagger UI for all API routes.
76
+
77
+ ### `GET /docs/json`
78
+
79
+ - Auth: none
80
+ - Description: Generated OpenAPI JSON document used by Swagger UI.
81
+
82
+ ### `GET /`
83
+
84
+ - Auth: none
85
+ - Response `200`: `{ "hello": "world" }`
86
+
87
+ ### `GET /auth/nonce`
88
+
89
+ - Auth: none
90
+ - Query params:
91
+ - `address` (required): EVM address, pattern `0x[a-fA-F0-9]{40}`
92
+ - Response `200`:
93
+
94
+ ```json
95
+ { "nonce": "<hex nonce>" }
96
+ ```
97
+
98
+ - Notes:
99
+ - Address is normalized to lowercase before nonce storage.
100
+ - Route-level rate limit uses `NONCE_RATE_LIMIT_MAX` and `NONCE_RATE_LIMIT_WINDOW_MS`.
101
+
102
+ ### `POST /auth/verify`
103
+
104
+ - Auth: none
105
+ - Body:
106
+
107
+ ```json
108
+ {
109
+ "message": "<SIWE message>",
110
+ "signature": "0x..."
111
+ }
112
+ ```
113
+
114
+ - Validation:
115
+ - `message`: string, 20-4096 chars
116
+ - `signature`: 132 chars, pattern `^0x[a-fA-F0-9]{130}$`
117
+ - Response `200`:
118
+
119
+ ```json
120
+ {
121
+ "accessToken": "<jwt>",
122
+ "refreshToken": "<refresh-token>"
123
+ }
124
+ ```
125
+
126
+ - Common errors:
127
+ - `400 { "error": "Invalid domain" }` when SIWE domain mismatches `ALLOWED_DOMAIN`
128
+ - `400 { "error": "Invalid chain" }` when `CHAIN_ID` is set and does not match
129
+ - `400 { "error": "Invalid nonce" }` when nonce is missing/consumed/mismatched
130
+ - `401 { "error": "Authentication failed" }` when SIWE message/signature verification fails
131
+ - Notes:
132
+ - Route-level rate limit uses `VERIFY_RATE_LIMIT_MAX` and `VERIFY_RATE_LIMIT_WINDOW_MS`.
133
+
134
+ ### `POST /auth/refresh`
135
+
136
+ - Auth: none
137
+ - Body:
138
+
139
+ ```json
140
+ {
141
+ "refreshToken": "<refresh-token>"
142
+ }
143
+ ```
144
+
145
+ - Validation:
146
+ - `refreshToken`: string, 16-2048 chars
147
+ - Response `200`:
148
+
149
+ ```json
150
+ {
151
+ "accessToken": "<jwt>",
152
+ "refreshToken": "<new-refresh-token>"
153
+ }
154
+ ```
155
+
156
+ - Common errors:
157
+ - `401 { "error": "Invalid refresh token" }`
158
+ - `500 { "error": "Unable to refresh token at this time" }`
159
+ - Notes:
160
+ - Route-level rate limit uses `REFRESH_RATE_LIMIT_MAX` and `REFRESH_RATE_LIMIT_WINDOW_MS`.
161
+
162
+ ### `GET /auth/me`
163
+
164
+ - Auth: `Authorization: Bearer <accessToken>`
165
+ - Guard behavior:
166
+ - JWT must be valid
167
+ - Session must still be active (not revoked/expired)
168
+ - Response `200`:
169
+
170
+ ```json
171
+ {
172
+ "address": "0x...",
173
+ "ensName": "alice.eth",
174
+ "firstSeen": "2026-01-01T00:00:00.000Z",
175
+ "sessionCount": 3
176
+ }
177
+ ```
178
+
179
+ - Common errors:
180
+ - `401 { "error": "Missing token" }`
181
+ - `401 { "error": "Invalid token" }`
182
+
183
+ ### `DELETE /auth/logout`
184
+
185
+ - Auth: `Authorization: Bearer <accessToken>`
186
+ - Guard behavior:
187
+ - JWT must be valid
188
+ - Session must still be active
189
+ - Response `200`:
190
+
191
+ ```json
192
+ { "success": true }
193
+ ```
194
+
195
+ - Common errors:
196
+ - `401 { "error": "Missing token" }`
197
+ - `401 { "error": "Invalid token" }`
198
+
199
+ ### Global behavior
200
+
201
+ - Global rate limit is enabled via:
202
+ - `RATE_LIMIT_MAX`
203
+ - `RATE_LIMIT_WINDOW_MS`
204
+ - Request body size is limited by `BODY_LIMIT_BYTES`.
205
+
206
+ ## Self-host
207
+
208
+ This project can be run as a standalone auth service. The application listens on port `3000`, runs migrations on startup, and requires PostgreSQL and Redis.
209
+
210
+ Release builds are published as GitHub Container Registry images on each release. Use the release tag you want to pin to, or `latest` if you intentionally want the most recent release.
211
+
212
+ ### Docker image
213
+
214
+ ```bash
215
+ docker pull ghcr.io/ajeanselme/fastify-siwe-middleware:latest
216
+ ```
217
+
218
+ ### Required environment variables
219
+
220
+ The service requires these environment variables at startup:
221
+
222
+ - `DB_HOST`
223
+ - `DB_PORT`
224
+ - `DB_USER`
225
+ - `DB_PASSWORD`
226
+ - `DB_DATABASE`
227
+ - `REDIS_HOST`
228
+ - `JWT_SECRET`
229
+ - `ALLOWED_DOMAIN`
230
+
231
+ Optional variables:
232
+
233
+ - `DB_SCHEMA`
234
+ - `REDIS_PORT`
235
+ - `REDIS_PASSWORD`
236
+ - `REDIS_DATABASE`
237
+ - `CHAIN_ID`
238
+ - `RPC_URL`
239
+ - `BODY_LIMIT_BYTES`
240
+ - `RATE_LIMIT_MAX`
241
+ - `RATE_LIMIT_WINDOW_MS`
242
+ - `NONCE_RATE_LIMIT_MAX`
243
+ - `NONCE_RATE_LIMIT_WINDOW_MS`
244
+ - `VERIFY_RATE_LIMIT_MAX`
245
+ - `VERIFY_RATE_LIMIT_WINDOW_MS`
246
+ - `REFRESH_RATE_LIMIT_MAX`
247
+ - `REFRESH_RATE_LIMIT_WINDOW_MS`
248
+ - `REFRESH_TOKEN_TTL_SECONDS`
249
+
250
+ ### Example compose setup
251
+
252
+ ```yaml
253
+ services:
254
+ app:
255
+ image: ghcr.io/ajeanselme/fastify-siwe-middleware:latest
256
+ ports: ["3000:3000"]
257
+ env_file: .env
258
+ depends_on: [postgres, redis]
259
+
260
+ postgres:
261
+ image: postgres:16-alpine
262
+ environment:
263
+ POSTGRES_DB: siwe_auth
264
+ POSTGRES_USER: siwe
265
+ POSTGRES_PASSWORD: secret
266
+ volumes: [pg_data:/var/lib/postgresql/data]
267
+
268
+ redis:
269
+ image: redis:7-alpine
270
+ command: redis-server --save ""
271
+
272
+ volumes:
273
+ pg_data:
274
+ ```
275
+
276
+ If you prefer local builds, the included [docker-compose.yml](docker-compose.yml) still works unchanged.
277
+
278
+ ## Build
279
+
280
+ ```bash
281
+ npm run build
282
+ ```
283
+
284
+ This emits ESM, CommonJS, and declaration files in `dist/`.