instaserve 1.1.12 → 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
 
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,3 +1,16 @@
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
+
1
14
  /**
2
15
  * Auth0 Authentication Module
3
16
  *
@@ -12,12 +25,13 @@
12
25
  * - GET /logout: Clears session and token.
13
26
  * - GET /callback: Handles Auth0 callback, validates token, creates user in DB.
14
27
  */
28
+
15
29
  export default {
16
30
  "GET /login": (req, res) => {
17
31
  const REDIRECT_URI = `https://${req.headers.host}/callback`;
18
32
  const state = crypto.randomBytes(16).toString("hex");
19
33
  global.a0state = state;
20
- 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)}`;
21
35
  res.writeHead(302, { Location: url });
22
36
  res.end();
23
37
  },
@@ -28,32 +42,37 @@ export default {
28
42
  global.sqlite.prepare("UPDATE users SET token = NULL WHERE token = ?").run(token);
29
43
  }
30
44
  res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=Lax");
31
- res.writeHead(302, { Location: "/" });
45
+ res.writeHead(302, { Location: LOGGEDOUT_REDIRECT });
32
46
  res.end();
33
47
  },
34
48
  "GET /callback": async (req, res, data) => {
35
49
  const REDIRECT_URI = `https://${req.headers.host}/callback`;
36
- const tokenRes = await fetch(`https://${secrets.auth0_domain}/oauth/token`, {
50
+ const tokenRes = await fetch(`https://${auth0_domain}/oauth/token`, {
37
51
  method: "POST",
38
52
  headers: { "content-type": "application/json" },
39
53
  body: JSON.stringify({
40
54
  grant_type: "authorization_code",
41
- client_id: secrets.auth0_clientid,
42
- client_secret: secrets.auth0_clientsecret,
55
+ client_id: auth0_clientid,
56
+ client_secret: auth0_clientsecret,
43
57
  code: data.code,
44
58
  redirect_uri: REDIRECT_URI
45
59
  }),
46
- })
60
+ });
61
+
47
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
+ }
48
67
  // decrypt token
49
68
  const id_token = tokens.id_token;
50
69
  const payload = JSON.parse(Buffer.from(id_token.split('.')[1], 'base64').toString());
51
70
  const auth_token = crypto.randomBytes(32).toString("hex");
52
- 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 = ?")
53
72
  .run(payload.email, payload.name, auth_token, auth_token, payload.name, payload.picture);
54
73
  res.setHeader("Set-Cookie", `username=${payload.email}; Path=/;`);
55
74
  res.setHeader("Set-Cookie", `name=${payload.name}; Path=/;`);
56
75
  res.setHeader("Set-Cookie", `token=${auth_token}; Path=/;`);
57
- 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>`;
58
77
  }
59
78
  }
package/lib/r2.js CHANGED
@@ -1,3 +1,15 @@
1
+ import https from 'https';
2
+ import crypto from 'crypto';
3
+ import { URL } from 'url';
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
+
1
13
  /**
2
14
  * Cloudflare R2 / S3-Compatible Storage Module
3
15
  *
@@ -19,9 +31,6 @@
19
31
  * Contains helper handlers for file operations. Note that `upload`, `download`, `delete`
20
32
  * are generic async functions awaiting `data` parameters, while `POST /files` is a standard route.
21
33
  */
22
- import https from 'https';
23
- import crypto from 'crypto';
24
- import { URL } from 'url';
25
34
 
26
35
  // --- Core SigV4 Utilities ---
27
36
 
@@ -198,6 +207,12 @@ function getSignedDownloadUrl(bucket, key, secrets) {
198
207
  return signR2('GET', bucket, key, null, ...getSecrets(secrets)).url;
199
208
  }
200
209
 
210
+ const defaultSecrets = {
211
+ cloudflareAccessKeyId,
212
+ cloudflareSecretAccessKey,
213
+ cloudflareAccountId
214
+ };
215
+
201
216
  export const fileRoutes = {
202
217
  upload: async (req, res, data) => {
203
218
  try {
@@ -209,27 +224,27 @@ export const fileRoutes = {
209
224
  } else {
210
225
  body = Buffer.alloc(0);
211
226
  }
212
- 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);
213
228
  } catch (e) { return { error: e.message }; }
214
229
  },
215
230
  download: async (req, res, data) => {
216
231
  try {
217
- return await downloadFromR2(data.bucket, data.key, data.secrets);
232
+ return await downloadFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
218
233
  } catch (e) { return { error: e.message }; }
219
234
  },
220
235
  delete: async (req, res, data) => {
221
236
  try {
222
- return await deleteFromR2(data.bucket, data.key, data.secrets);
237
+ return await deleteFromR2(data.bucket, data.key, data.secrets || defaultSecrets);
223
238
  } catch (e) { return { error: e.message }; }
224
239
  },
225
240
  "POST /files": async (req, res, data) => {
226
241
  try {
227
- return await listR2Files(data.bucket, data.prefix, data.secrets);
242
+ return await listR2Files(data.bucket, data.prefix, data.secrets || defaultSecrets);
228
243
  } catch (e) { return { error: e.message }; }
229
244
  }
230
245
  };
231
246
 
232
- export {
247
+ export default {
233
248
  uploadToR2,
234
249
  downloadFromR2,
235
250
  deleteFromR2,
package/lib/sqlite.js CHANGED
@@ -17,10 +17,8 @@
17
17
  * - GET /all: List all keys/values for the user.
18
18
  * - DELETE /api: Delete a key.
19
19
  */
20
+
20
21
  import Database from "better-sqlite3";
21
- import { fileRoutes } from "./lib/file-routes.js";
22
- import secrets from "./secrets.js";
23
- import crypto from "crypto";
24
22
 
25
23
  // Initialize database
26
24
  global.sqlite = new Database("data.db");
@@ -30,8 +28,8 @@ sqlite.prepare("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)
30
28
  // Combine all routes
31
29
  export default {
32
30
  _auth: (req, res, data) => {
33
- if (req.url.match(/js|html|css/)) return;
34
- if (req.url == "/") return 401;
31
+ if (req.url === '/' || req.url.match(/js|html|css|callback/)) return;
32
+
35
33
  if (req.url.startsWith("/register") || req.url.startsWith("/login")) return;
36
34
 
37
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.12",
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