instaserve 1.1.11 → 1.1.14

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 CHANGED
@@ -8,10 +8,27 @@
8
8
  </p>
9
9
  </div>
10
10
 
11
+ ## Configuration
12
+
13
+ Instaserve uses environment variables for Auth0 and Cloudflare R2 integration. Create a `.env` file (if your runner supports it) or pass them before the command.
14
+
15
+ ### Auth0 Variables
16
+ - `AUTH0_DOMAIN`: Your Auth0 domain
17
+ - `AUTH0_CLIENT_ID`: Your Auth0 Client ID
18
+ - `AUTH0_CLIENT_SECRET`: Your Auth0 Client Secret
19
+
20
+ ### Cloudflare R2 Variables (via `lib/r2.js`)
21
+ - `CLOUDFLARE_ACCOUNT_ID`: R2 Account ID
22
+ - `CLOUDFLARE_ACCESS_KEY_ID`: R2 Access Key ID
23
+ - `CLOUDFLARE_SECRET_ACCESS_KEY`: R2 Secret Access Key
24
+
11
25
  ## Usage
12
26
 
13
27
  <div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); margin: 15px 0;">
14
- <pre style="margin: 0;"><code>npx instaserve [options]
28
+ <pre style="margin: 0;"><code># Run with env vars
29
+ AUTH0_DOMAIN=... AUTH0_CLIENT_ID=... npx instaserve [options]
30
+
31
+ # Generate routes file
15
32
  npx instaserve generate-routes</code></pre>
16
33
  </div>
17
34
 
@@ -238,3 +255,49 @@ export default {
238
255
  }
239
256
  }
240
257
  ```
258
+
259
+ ## Library Modules
260
+
261
+ The `lib/` directory contains useful modules for authentication, storage, and database manipulation.
262
+
263
+ ### Auth0 (`lib/auth0.js`)
264
+ Provides authentication routes and `req.user` handling via Auth0.
265
+ - **Routes:** `GET /login`, `GET /logout`, `GET /callback`
266
+ - **Usage:** Import and spread into your routes.
267
+ ```javascript
268
+ import auth0 from './lib/auth0.js';
269
+
270
+ export default {
271
+ ...auth0,
272
+ // other routes
273
+ }
274
+ ```
275
+
276
+ ### R2 Storage (`lib/r2.js`)
277
+ Utilities for Cloudflare R2 or S3-compatible object storage.
278
+ - **Features:** `uploadToR2`, `downloadFromR2`, `listR2Files`
279
+ - **Routes:** `fileRoutes` exports helpers for file management.
280
+ - **Usage:**
281
+ ```javascript
282
+ import { fileRoutes } from './lib/r2.js';
283
+
284
+ export default {
285
+ ...fileRoutes,
286
+ // other routes
287
+ }
288
+ ```
289
+
290
+ ### SQLite (`lib/sqlite.js`)
291
+ Provides a local SQLite database with a per-user Key-Value store.
292
+ - **Features:** User management table, KV table.
293
+ - **Routes:** `_auth` middleware, CRUD for KV store (`/api`, `/all`).
294
+ - **Usage:**
295
+ ```javascript
296
+ import sqliteRoutes from './lib/sqlite.js';
297
+
298
+ export default {
299
+ ...sqliteRoutes,
300
+ // other routes
301
+ }
302
+ ```
303
+ ```
package/api.txt ADDED
@@ -0,0 +1,61 @@
1
+ Instaserve API Documentation
2
+
3
+ Authentication
4
+ --------------
5
+ GET /login
6
+ Redirects to Auth0 for sign-in.
7
+ Sets a secure http-only session cookie ('token') upon success.
8
+
9
+ GET /logout
10
+ Logs the user out and clears the session.
11
+
12
+ Key-Value Store (SQLite)
13
+ ------------------------
14
+ * All endpoints require authentication.
15
+ * Data is isolated per user.
16
+
17
+ POST /api
18
+ Set a key-value pair.
19
+ Body (JSON): { "key": "myKey", "value": "myValue" }
20
+ Returns: { "key": "myKey", "value": "myValue" }
21
+
22
+ GET /api?key=myKey
23
+ Retrieve a value by key.
24
+ Returns: { "key": "myKey", "value": "myValue" }
25
+
26
+ GET /all
27
+ List all key-value pairs for the current user.
28
+ Returns: [ { "key": "key1", "value": "val1" }, ... ]
29
+
30
+ DELETE /api
31
+ Delete a key.
32
+ Body (JSON): { "key": "myKey" }
33
+ Returns: { "deleted": true, "key": "myKey" }
34
+
35
+ Storage (R2 / S3)
36
+ -----------------
37
+ * Requires R2 environment variables to be set on the server.
38
+ * Uses the credentials provided in endpoints or server defaults.
39
+
40
+ POST /upload
41
+ Upload a file.
42
+ Body (JSON):
43
+ {
44
+ "bucket": "my-bucket-name",
45
+ "key": "filename.jpg",
46
+ "body": "base64_encoded_string_contents",
47
+ "contentType": "image/jpeg"
48
+ }
49
+
50
+ POST /files
51
+ List files in a bucket.
52
+ Body (JSON): { "bucket": "my-bucket-name" }
53
+ Returns: List of file objects (key, size, lastModified).
54
+
55
+ GET /download?bucket=my-bucket-name&key=filename.jpg
56
+ Download a file directly.
57
+ (Available via the 'download' route handler)
58
+
59
+ GET /delete?bucket=my-bucket-name&key=filename.jpg
60
+ Delete a file.
61
+ (Available via the 'delete' route handler)
package/lib/auth0.js CHANGED
@@ -1,9 +1,37 @@
1
+ import crypto from "crypto";
2
+
3
+ const auth0_domain = process.env.AUTH0_DOMAIN || process.env.auth0_domain;
4
+ const auth0_clientid = process.env.AUTH0_CLIENT_ID || process.env.auth0_clientid;
5
+ const auth0_clientsecret = process.env.AUTH0_CLIENT_SECRET || process.env.auth0_clientsecret;
6
+
7
+ const LOGGEDIN_REDIRECT = "/";
8
+ const LOGGEDOUT_REDIRECT = "/";
9
+
10
+ if (!auth0_domain || !auth0_clientid || !auth0_clientsecret) {
11
+ console.warn("Auth0 environment variables not set! Please set AUTH0_DOMAIN, AUTH0_CLIENT_ID, and AUTH0_CLIENT_SECRET.");
12
+ }
13
+
14
+ /**
15
+ * Auth0 Authentication Module
16
+ *
17
+ * Usage:
18
+ * 1. Import this module in your routes.js file.
19
+ * 2. Merge it into your default export routes object: `...auth0`.
20
+ * 3. Ensure 'secrets' is globally available or imported with `auth0_domain`, `auth0_clientid`, and `auth0_clientsecret`.
21
+ * 4. Ensure `global.sqlite` is initialized (typically by importing lib/sqlite.js).
22
+ *
23
+ * Routes provided:
24
+ * - GET /login: Initiates Auth0 login flow.
25
+ * - GET /logout: Clears session and token.
26
+ * - GET /callback: Handles Auth0 callback, validates token, creates user in DB.
27
+ */
28
+
1
29
  export default {
2
30
  "GET /login": (req, res) => {
3
- const REDIRECT_URI = "https://${req.headers.host}/callback";
31
+ const REDIRECT_URI = `https://${req.headers.host}/callback`;
4
32
  const state = crypto.randomBytes(16).toString("hex");
5
33
  global.a0state = state;
6
- const url = `https://${secrets.auth0_domain}/authorize?response_type=code&client_id=${secrets.auth0_clientid}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=openid%20profile%20email&state=${encodeURIComponent(state)}`;
34
+ const url = `https://${auth0_domain}/authorize?response_type=code&client_id=${auth0_clientid}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&scope=openid%20profile%20email&state=${encodeURIComponent(state)}`;
7
35
  res.writeHead(302, { Location: url });
8
36
  res.end();
9
37
  },
@@ -14,32 +42,37 @@ export default {
14
42
  global.sqlite.prepare("UPDATE users SET token = NULL WHERE token = ?").run(token);
15
43
  }
16
44
  res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=Lax");
17
- res.writeHead(302, { Location: "/" });
45
+ res.writeHead(302, { Location: LOGGEDOUT_REDIRECT });
18
46
  res.end();
19
47
  },
20
48
  "GET /callback": async (req, res, data) => {
21
- const REDIRECT_URI = "https://${req.headers.host}/callback";
22
- const tokenRes = await fetch("https://digplan.auth0.com/oauth/token", {
49
+ const REDIRECT_URI = `https://${req.headers.host}/callback`;
50
+ const tokenRes = await fetch(`https://${auth0_domain}/oauth/token`, {
23
51
  method: "POST",
24
52
  headers: { "content-type": "application/json" },
25
53
  body: JSON.stringify({
26
54
  grant_type: "authorization_code",
27
- client_id: secrets.auth0_clientid,
28
- client_secret: secrets.auth0_clientsecret,
55
+ client_id: auth0_clientid,
56
+ client_secret: auth0_clientsecret,
29
57
  code: data.code,
30
58
  redirect_uri: REDIRECT_URI
31
59
  }),
32
- })
60
+ });
61
+
33
62
  const tokens = await tokenRes.json();
63
+ if (tokens.error || !tokens.id_token) {
64
+ console.error("Auth0 Error:", tokens);
65
+ return `Auth0 Error: ${tokens.error_description || tokens.error || "Unknown error"}`;
66
+ }
34
67
  // decrypt token
35
68
  const id_token = tokens.id_token;
36
69
  const payload = JSON.parse(Buffer.from(id_token.split('.')[1], 'base64').toString());
37
70
  const auth_token = crypto.randomBytes(32).toString("hex");
38
- sqlite.prepare("INSERT INTO users (id, username, token) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET token = ?, name = ?, picture = ?")
71
+ global.sqlite.prepare("INSERT INTO users (id, username, token) VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET token = ?, name = ?, picture = ?")
39
72
  .run(payload.email, payload.name, auth_token, auth_token, payload.name, payload.picture);
40
73
  res.setHeader("Set-Cookie", `username=${payload.email}; Path=/;`);
41
74
  res.setHeader("Set-Cookie", `name=${payload.name}; Path=/;`);
42
75
  res.setHeader("Set-Cookie", `token=${auth_token}; Path=/;`);
43
- return `<script>localStorage.setItem('name', '${payload.name}');localStorage.setItem('pic', '${payload.picture}');window.location.href = '/';</script>`;
76
+ return `<script>localStorage.setItem('name', '${payload.name}');localStorage.setItem('pic', '${payload.picture}');window.location.href = '${LOGGEDIN_REDIRECT}';</script>`;
44
77
  }
45
78
  }
package/lib/r2.js CHANGED
@@ -2,6 +2,36 @@ import https from 'https';
2
2
  import crypto from 'crypto';
3
3
  import { URL } from 'url';
4
4
 
5
+ const cloudflareAccessKeyId = process.env.CLOUDFLARE_ACCESS_KEY_ID || process.env.cloudflareAccessKeyId;
6
+ const cloudflareSecretAccessKey = process.env.CLOUDFLARE_SECRET_ACCESS_KEY || process.env.cloudflareSecretAccessKey;
7
+ const cloudflareAccountId = process.env.CLOUDFLARE_ACCOUNT_ID || process.env.cloudflareAccountId;
8
+
9
+ if (!cloudflareAccessKeyId || !cloudflareSecretAccessKey || !cloudflareAccountId) {
10
+ console.warn("Cloudflare environment variables not set! Please set CLOUDFLARE_ACCESS_KEY_ID, CLOUDFLARE_SECRET_ACCESS_KEY, and CLOUDFLARE_ACCOUNT_ID.");
11
+ }
12
+
13
+ /**
14
+ * Cloudflare R2 / S3-Compatible Storage Module
15
+ *
16
+ * Usage:
17
+ * 1. Import desired functions or `fileRoutes` from this file.
18
+ * 2. Requires a `secrets` object with:
19
+ * - cloudflareAccessKeyId
20
+ * - cloudflareSecretAccessKey
21
+ * - cloudflareAccountId
22
+ *
23
+ * Exports:
24
+ * - uploadToR2(bucket, key, body, contentType, secrets)
25
+ * - downloadFromR2(bucket, key, secrets)
26
+ * - deleteFromR2(bucket, key, secrets)
27
+ * - listR2Files(bucket, prefix, secrets)
28
+ * - getSignedDownloadUrl(bucket, key, secrets)
29
+ *
30
+ * fileRoutes:
31
+ * Contains helper handlers for file operations. Note that `upload`, `download`, `delete`
32
+ * are generic async functions awaiting `data` parameters, while `POST /files` is a standard route.
33
+ */
34
+
5
35
  // --- Core SigV4 Utilities ---
6
36
 
7
37
  const hashSHA256 = (str) => crypto.createHash('sha256').update(str).digest('hex');
@@ -177,6 +207,12 @@ function getSignedDownloadUrl(bucket, key, secrets) {
177
207
  return signR2('GET', bucket, key, null, ...getSecrets(secrets)).url;
178
208
  }
179
209
 
210
+ const defaultSecrets = {
211
+ cloudflareAccessKeyId,
212
+ cloudflareSecretAccessKey,
213
+ cloudflareAccountId
214
+ };
215
+
180
216
  export const fileRoutes = {
181
217
  upload: async (req, res, data) => {
182
218
  try {
@@ -188,27 +224,27 @@ export const fileRoutes = {
188
224
  } else {
189
225
  body = Buffer.alloc(0);
190
226
  }
191
- return await uploadToR2(data.bucket, data.key, body, data.contentType, data.secrets);
227
+ return await uploadToR2(data.bucket, data.key, body, data.contentType, data.secrets || defaultSecrets);
192
228
  } catch (e) { return { error: e.message }; }
193
229
  },
194
230
  download: async (req, res, data) => {
195
231
  try {
196
- return await downloadFromR2(data.bucket, data.key, data.secrets);
232
+ return await downloadFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
197
233
  } catch (e) { return { error: e.message }; }
198
234
  },
199
235
  delete: async (req, res, data) => {
200
236
  try {
201
- return await deleteFromR2(data.bucket, data.key, data.secrets);
237
+ return await deleteFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
202
238
  } catch (e) { return { error: e.message }; }
203
239
  },
204
240
  "POST /files": async (req, res, data) => {
205
241
  try {
206
- return await listR2Files(data.bucket, data.prefix, data.secrets);
242
+ return await listR2Files(data.bucket, data.prefix, data.secrets || defaultSecrets);
207
243
  } catch (e) { return { error: e.message }; }
208
244
  }
209
245
  };
210
246
 
211
- export {
247
+ export default {
212
248
  uploadToR2,
213
249
  downloadFromR2,
214
250
  deleteFromR2,
package/lib/sqlite.js CHANGED
@@ -1,7 +1,24 @@
1
+ /**
2
+ * SQLite Database & Key-Value Store Module
3
+ *
4
+ * Usage:
5
+ * 1. Import this module to initialize the database ('data.db') and tables ('users', 'kv').
6
+ * 2. Access the database instance via `global.sqlite`.
7
+ * 3. Exported routes provide a per-user Key-Value store mechanism guarded by the `_auth` middleware.
8
+ *
9
+ * Tables created:
10
+ * - users: id, username, pass, token, picture, name, email
11
+ * - kv: key (format: "username:key"), value
12
+ *
13
+ * Routes provided:
14
+ * - _auth: Middleware for protecting routes (assigns `req.user`).
15
+ * - POST /api: Set a KV pair for the logged-in user.
16
+ * - GET /api: Get a value by key.
17
+ * - GET /all: List all keys/values for the user.
18
+ * - DELETE /api: Delete a key.
19
+ */
20
+
1
21
  import Database from "better-sqlite3";
2
- import { fileRoutes } from "./lib/file-routes.js";
3
- import secrets from "./secrets.js";
4
- import crypto from "crypto";
5
22
 
6
23
  // Initialize database
7
24
  global.sqlite = new Database("data.db");
@@ -11,8 +28,8 @@ sqlite.prepare("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)
11
28
  // Combine all routes
12
29
  export default {
13
30
  _auth: (req, res, data) => {
14
- if (req.url.match(/js|html|css/)) return;
15
- if (req.url == "/") return 401;
31
+ if (req.url === '/' || req.url.match(/js|html|css|callback/)) return;
32
+
16
33
  if (req.url.startsWith("/register") || req.url.startsWith("/login")) return;
17
34
 
18
35
  const token = req.headers.cookie?.split('token=')[1].split(';')[0];
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "instaserve",
3
- "version": "1.1.11",
3
+ "version": "1.1.14",
4
4
  "description": "Instant web stack",
5
5
  "main": "module.mjs",
6
6
  "bin": "./instaserve",
7
7
  "scripts": {
8
- "start": "node instaserve"
8
+ "start": "node instaserve",
9
+ "test": "./test_api.sh"
9
10
  },
10
11
  "type": "module",
11
12
  "dependencies": {
package/routes-full.js ADDED
@@ -0,0 +1,12 @@
1
+ import auth0 from "./lib/auth0.js";
2
+ import { fileRoutes } from "./lib/r2.js";
3
+ import sqlite from "./lib/sqlite.js";
4
+
5
+ export default {
6
+ _log: (req, res) => {
7
+ console.log(req.method, req.url);
8
+ },
9
+ ...auth0,
10
+ ...fileRoutes,
11
+ ...sqlite
12
+ };
package/test_api.sh ADDED
@@ -0,0 +1,170 @@
1
+ #!/bin/bash
2
+
3
+ # Configuration
4
+ PORT=3001
5
+ BASE_URL="https://127.0.0.1:$PORT"
6
+ DB_FILE="data.db"
7
+ TEST_USER="testuser"
8
+ TEST_TOKEN="test_token_12345"
9
+ TEST_KEY="test_key"
10
+ TEST_VALUE="test_value"
11
+ SERVER_PID=""
12
+
13
+ # Helper function to run sqlite commands
14
+ run_sqlite() {
15
+ sqlite3 "$DB_FILE" "$1"
16
+ }
17
+
18
+ # Cleanup function to kill server and remove test data
19
+ cleanup() {
20
+ echo ""
21
+ echo "--- Cleanup ---"
22
+
23
+ if [ -n "$SERVER_PID" ]; then
24
+ echo "Stopping server (PID: $SERVER_PID)..."
25
+ kill "$SERVER_PID" 2>/dev/null
26
+ fi
27
+
28
+ echo "Removing test data..."
29
+ run_sqlite "DELETE FROM users WHERE id = 'test_id';"
30
+ # run_sqlite "DELETE FROM kv WHERE key = '$TEST_USER:$TEST_KEY';"
31
+
32
+ rm -f server.log
33
+ echo "Done."
34
+ }
35
+
36
+ # Register cleanup to run on script exit (success or failure)
37
+ trap cleanup EXIT
38
+
39
+ # 1. Start the Server
40
+ echo "--- Setup ---"
41
+ echo "Starting Instaserve on port $PORT..."
42
+ # Start server in background, directing output to log
43
+ ./instaserve -api ./routes-full.js -secure -port "$PORT" > server.log 2>&1 &
44
+ SERVER_PID=$!
45
+ echo "Server process ID: $SERVER_PID"
46
+
47
+ # Wait for server to be ready
48
+ echo "Waiting for server to start..."
49
+ MAX_RETRIES=10
50
+ COUNT=0
51
+ STARTED=false
52
+
53
+ while [ $COUNT -lt $MAX_RETRIES ]; do
54
+ if grep -q "started on:" server.log; then
55
+ STARTED=true
56
+ break
57
+ fi
58
+ sleep 1
59
+ ((COUNT++))
60
+ echo -n "."
61
+ done
62
+ echo ""
63
+
64
+ if [ "$STARTED" = false ]; then
65
+ echo "FAIL: Server failed to start within timeout."
66
+ echo "Server Log:"
67
+ cat server.log
68
+ exit 1
69
+ fi
70
+
71
+ echo "Server is running!"
72
+
73
+ # 2. Insert Test User
74
+ echo "Setting up test user in database..."
75
+ # Ensure tables exist (the server creation might race with this if it's the very first run,
76
+ # but server is confirmed started above, so tables should be there)
77
+ run_sqlite "INSERT OR REPLACE INTO users (id, username, token, name, email) VALUES ('test_id', '$TEST_USER', '$TEST_TOKEN', 'Test User', 'test@example.com');"
78
+
79
+
80
+ # 3. Validation Tests
81
+ echo ""
82
+ echo "--- Running Tests ---"
83
+
84
+ # Test 1: Unauthenticated Access
85
+ echo "- Testing unauthenticated access to /api (Expect 401)..."
86
+ HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/api")
87
+ if [ "$HTTP_CODE" == "401" ]; then
88
+ echo " ✅ PASS"
89
+ else
90
+ echo " ❌ FAIL: Returned $HTTP_CODE"
91
+ fi
92
+
93
+ # Test 2: POST /api (Set Key)
94
+ echo "- Testing POST /api (Set Key)..."
95
+ RESPONSE=$(curl -k -s -X POST "$BASE_URL/api" \
96
+ -H "Cookie: token=$TEST_TOKEN" \
97
+ -H "Content-Type: application/json" \
98
+ -d "{\"key\": \"$TEST_KEY\", \"value\": \"$TEST_VALUE\"}")
99
+
100
+ if [[ "$RESPONSE" == *"$TEST_VALUE"* ]]; then
101
+ echo " ✅ PASS: $RESPONSE"
102
+ else
103
+ echo " ❌ FAIL: $RESPONSE"
104
+ fi
105
+
106
+ # Test 3: GET /api (Get Value)
107
+ echo "- Testing GET /api (Get Key)..."
108
+ RESPONSE=$(curl -k -s -G "$BASE_URL/api" \
109
+ -H "Cookie: token=$TEST_TOKEN" \
110
+ --data-urlencode "key=$TEST_KEY")
111
+
112
+ if [[ "$RESPONSE" == *"$TEST_VALUE"* ]]; then
113
+ echo " ✅ PASS: $RESPONSE"
114
+ else
115
+ echo " ❌ FAIL: $RESPONSE"
116
+ fi
117
+
118
+ # Test 4: GET /all (List Keys)
119
+ echo "- Testing GET /all..."
120
+ RESPONSE=$(curl -k -s "$BASE_URL/all" \
121
+ -H "Cookie: token=$TEST_TOKEN")
122
+
123
+ if [[ "$RESPONSE" == *"$TEST_KEY"* ]]; then
124
+ echo " ✅ PASS: Found key in list"
125
+ else
126
+ echo " ❌ FAIL: $RESPONSE"
127
+ fi
128
+
129
+ # 6. Test Authenticated Access (DELETE /api) - Delete Key
130
+ echo "- Testing DELETE /api..."
131
+ RESPONSE=$(curl -k -s -X DELETE "$BASE_URL/api" \
132
+ -H "Cookie: token=$TEST_TOKEN" \
133
+ -H "Content-Type: application/json" \
134
+ -d "{\"key\": \"$TEST_KEY\"}")
135
+
136
+ if [[ "$RESPONSE" == *"deleted"* ]]; then
137
+ echo " ✅ PASS: $RESPONSE"
138
+ else
139
+ echo " ❌ FAIL: $RESPONSE"
140
+ fi
141
+
142
+ # 7. Test Login Redirect
143
+ echo "- Testing GET /login (Expect 302 Redirect to Auth0)..."
144
+ LOGIN_HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/login")
145
+ if [ "$LOGIN_HTTP_CODE" == "302" ]; then
146
+ echo " ✅ PASS"
147
+ else
148
+ echo " ❌ FAIL: Returned $LOGIN_HTTP_CODE"
149
+ fi
150
+
151
+ # 8. Test Logout
152
+ echo "- Testing GET /logout (Expect 302 Redirect)..."
153
+ # Use -D - to dump headers, -o /dev/null to discard body
154
+ LOGOUT_HEADERS=$(curl -k -s -D - -o /dev/null "$BASE_URL/logout" -H "Cookie: token=$TEST_TOKEN")
155
+
156
+ if echo "$LOGOUT_HEADERS" | grep -q "302 Found"; then
157
+ echo " ✅ PASS: Redirect confirmed"
158
+ else
159
+ echo " ❌ FAIL: No redirect found in headers"
160
+ echo "$LOGOUT_HEADERS"
161
+ fi
162
+
163
+ echo "- Verifying token invalidation (Expect 401 on /api)..."
164
+ # Reuse TEST_TOKEN which should now be invalidated in DB
165
+ HTTP_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "$BASE_URL/api" -H "Cookie: token=$TEST_TOKEN")
166
+ if [ "$HTTP_CODE" == "401" ]; then
167
+ echo " ✅ PASS: Token no longer accepted"
168
+ else
169
+ echo " ❌ FAIL: Token still valid (Returned $HTTP_CODE)"
170
+ fi