instaserve 1.0.20 → 1.1.1
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 +145 -0
- package/generate-certs.sh +59 -0
- package/instaserve +51 -0
- package/module.mjs +136 -0
- package/package.json +14 -5
- package/public/index.html +209 -0
- package/routes.js +54 -0
- package/index.mjs +0 -49
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1 style="color: #3b82f6; margin: 10px 0 5px;">Instaserve</h1>
|
|
3
|
+
<p style="color: #6b7280; margin: 0;">Instant web stack for Node.js</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
<div style="background: white; padding: 15px; border-radius: 6px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); margin: 15px 0;">
|
|
9
|
+
<pre style="margin: 0;"><code>npx instaserve [options]</code></pre>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
## Options
|
|
13
|
+
|
|
14
|
+
<div style="display: flex; gap: 8px; margin: 8px 0;">
|
|
15
|
+
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 4px; color: #2563eb;">-port <number></code>
|
|
16
|
+
<span>Port to listen on (default: 3000)</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div style="display: flex; gap: 8px; margin: 8px 0;">
|
|
20
|
+
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 4px; color: #2563eb;">-ip <address></code>
|
|
21
|
+
<span>IP address to bind to (default: 127.0.0.1)</span>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div style="display: flex; gap: 8px; margin: 8px 0;">
|
|
25
|
+
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 4px; color: #2563eb;">-public <path></code>
|
|
26
|
+
<span>Public directory path (default: ./public)</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div style="display: flex; gap: 8px; margin: 8px 0;">
|
|
30
|
+
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 4px; color: #2563eb;">-api <file></code>
|
|
31
|
+
<span>Path to routes file (default: ./routes.js)</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div style="display: flex; gap: 8px; margin: 8px 0;">
|
|
35
|
+
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 4px; color: #2563eb;">-secure</code>
|
|
36
|
+
<span>Enable HTTPS (requires cert.pem and key.pem - run ./generate-certs.sh)</span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div style="display: flex; gap: 8px; margin: 8px 0;">
|
|
40
|
+
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 4px; color: #2563eb;">-help</code>
|
|
41
|
+
<span>Show help message</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
## HTTPS Support
|
|
45
|
+
|
|
46
|
+
Instaserve supports HTTPS with self-signed certificates. To enable HTTPS:
|
|
47
|
+
|
|
48
|
+
1. **Generate certificates:**
|
|
49
|
+
```bash
|
|
50
|
+
./generate-certs.sh
|
|
51
|
+
```
|
|
52
|
+
This creates `cert.pem` and `key.pem` files and adds them to your system's trust store.
|
|
53
|
+
|
|
54
|
+
2. **Run with HTTPS:**
|
|
55
|
+
```bash
|
|
56
|
+
npx instaserve -secure
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The certificate generation script:
|
|
60
|
+
- Creates a self-signed certificate valid for 365 days
|
|
61
|
+
- Automatically adds the certificate to your system trust store (macOS/Linux)
|
|
62
|
+
- Prevents browser security warnings
|
|
63
|
+
|
|
64
|
+
## Routes
|
|
65
|
+
|
|
66
|
+
The routes file (`routes.js` by default) defines your API endpoints. Each route is a function that handles requests to a specific URL path.
|
|
67
|
+
|
|
68
|
+
### Basic Route Example
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
export default {
|
|
72
|
+
// Handle GET /hello
|
|
73
|
+
hello: (req, res, data) => {
|
|
74
|
+
return { message: 'Hello World' }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Special Routes (Middleware)
|
|
80
|
+
|
|
81
|
+
Routes starting with `_` are middleware functions that run on **every request** before the main route handler. They are useful for:
|
|
82
|
+
|
|
83
|
+
- Logging requests
|
|
84
|
+
- Authentication
|
|
85
|
+
- Request modification
|
|
86
|
+
- Response headers
|
|
87
|
+
|
|
88
|
+
Middleware functions can:
|
|
89
|
+
- Return `false` to continue to the next middleware or main route
|
|
90
|
+
- Return a truthy value to stop processing and use that as the response
|
|
91
|
+
- Modify the request or response objects
|
|
92
|
+
|
|
93
|
+
#### Middleware Example
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
export default {
|
|
97
|
+
// Log every request
|
|
98
|
+
_log: (req, res, data) => {
|
|
99
|
+
console.log(`${req.method} ${req.url}`)
|
|
100
|
+
return false // Continue processing
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// Block unauthorized requests
|
|
104
|
+
_auth: (req, res, data) => {
|
|
105
|
+
if (!data.token) {
|
|
106
|
+
res.writeHead(401)
|
|
107
|
+
return 'Unauthorized'
|
|
108
|
+
}
|
|
109
|
+
return false // Continue if authorized
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Route Parameters
|
|
115
|
+
|
|
116
|
+
Each route function receives:
|
|
117
|
+
- `req` - The HTTP request object
|
|
118
|
+
- `res` - The HTTP response object
|
|
119
|
+
- `data` - Combined data from:
|
|
120
|
+
- POST body (if JSON)
|
|
121
|
+
- URL query parameters
|
|
122
|
+
- Form data
|
|
123
|
+
|
|
124
|
+
### Example Routes File
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
// routes.js
|
|
128
|
+
export default {
|
|
129
|
+
// Middleware example
|
|
130
|
+
_debug: (req, res, data) => {
|
|
131
|
+
console.log('Request:', req.url)
|
|
132
|
+
return false // Continue to next route
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// API endpoint
|
|
136
|
+
api: (req, res, data) => {
|
|
137
|
+
return { status: 'ok', data }
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Error handling
|
|
141
|
+
testerror: () => {
|
|
142
|
+
throw new Error('Test error')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
echo "Generating self-signed certificates for Instaserve..."
|
|
4
|
+
|
|
5
|
+
# Create openssl config file
|
|
6
|
+
cat > openssl.conf << EOF
|
|
7
|
+
[req]
|
|
8
|
+
distinguished_name = req_distinguished_name
|
|
9
|
+
req_extensions = v3_req
|
|
10
|
+
prompt = no
|
|
11
|
+
|
|
12
|
+
[req_distinguished_name]
|
|
13
|
+
C = US
|
|
14
|
+
ST = State
|
|
15
|
+
L = City
|
|
16
|
+
O = Instaserve
|
|
17
|
+
OU = Development
|
|
18
|
+
CN = localhost
|
|
19
|
+
|
|
20
|
+
[v3_req]
|
|
21
|
+
basicConstraints = CA:FALSE
|
|
22
|
+
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
|
23
|
+
extendedKeyUsage = serverAuth, clientAuth
|
|
24
|
+
subjectAltName = @alt_names
|
|
25
|
+
|
|
26
|
+
[alt_names]
|
|
27
|
+
DNS.1 = localhost
|
|
28
|
+
DNS.2 = *.localhost
|
|
29
|
+
IP.1 = 127.0.0.1
|
|
30
|
+
IP.2 = ::1
|
|
31
|
+
EOF
|
|
32
|
+
|
|
33
|
+
# Generate private key
|
|
34
|
+
openssl genrsa -out key.pem 2048
|
|
35
|
+
|
|
36
|
+
# Generate certificate with config
|
|
37
|
+
openssl req -new -x509 -key key.pem -out cert.pem -days 365 -config openssl.conf -extensions v3_req
|
|
38
|
+
|
|
39
|
+
# Clean up config
|
|
40
|
+
rm openssl.conf
|
|
41
|
+
|
|
42
|
+
echo "Certificates generated: cert.pem and key.pem"
|
|
43
|
+
|
|
44
|
+
# Add to system trust store (macOS)
|
|
45
|
+
if [[ "$OSTYPE" == "darwin"* ]]; then
|
|
46
|
+
echo "Adding certificate to macOS trust store..."
|
|
47
|
+
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain cert.pem
|
|
48
|
+
echo "Certificate added to macOS trust store"
|
|
49
|
+
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
|
50
|
+
echo "Adding certificate to Linux trust store..."
|
|
51
|
+
sudo cp cert.pem /usr/local/share/ca-certificates/instaserve.crt
|
|
52
|
+
sudo update-ca-certificates
|
|
53
|
+
echo "Certificate added to Linux trust store"
|
|
54
|
+
else
|
|
55
|
+
echo "Please manually add cert.pem to your system's trust store"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo "HTTPS certificates ready! Use 'npx instaserve -secure' to enable HTTPS"
|
|
59
|
+
echo "Note: You may still see browser warnings for self-signed certificates"
|
package/instaserve
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
|
|
6
|
+
console.log(chalk.cyan('\nInstaserve - Instant Web Stack\n'))
|
|
7
|
+
console.log(chalk.yellow('Usage:'))
|
|
8
|
+
console.log(chalk.green(' npx instaserve [options]\n'))
|
|
9
|
+
console.log(chalk.yellow('Options:'))
|
|
10
|
+
console.log(chalk.green(' -port <number>') + ' Port to listen on (default: 3000)')
|
|
11
|
+
console.log(chalk.green(' -ip <address>') + ' IP address to bind to (default: 127.0.0.1)')
|
|
12
|
+
console.log(chalk.green(' -public <path>') + ' Public directory path (default: ./public)')
|
|
13
|
+
console.log(chalk.green(' -api <file>') + ' Path to routes file (default: ./routes.js)')
|
|
14
|
+
console.log(chalk.green(' -secure') + ' Enable HTTPS (requires cert.pem and key.pem: run generate-certs.sh)')
|
|
15
|
+
console.log(chalk.green(' -help') + ' Show this help message\n')
|
|
16
|
+
|
|
17
|
+
if (process.argv.includes('-help')) {
|
|
18
|
+
process.exit(0)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
import server from './module.mjs'
|
|
22
|
+
|
|
23
|
+
const args = process.argv.slice(2)
|
|
24
|
+
const params = {}
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i]
|
|
27
|
+
if (arg.startsWith('-')) {
|
|
28
|
+
const key = arg.slice(1)
|
|
29
|
+
const nextArg = args[i + 1]
|
|
30
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
31
|
+
params[key] = nextArg
|
|
32
|
+
i++ // Skip the next argument since we used it
|
|
33
|
+
} else {
|
|
34
|
+
params[key] = true // Boolean flag
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load routes file
|
|
40
|
+
let routes = {}
|
|
41
|
+
const routesFile = params.api || './routes.js'
|
|
42
|
+
if (fs.existsSync(routesFile)) {
|
|
43
|
+
try {
|
|
44
|
+
const imported = await import(routesFile)
|
|
45
|
+
routes = imported.default || imported
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.log(chalk.yellow(`Warning: Could not load routes file ${routesFile}: ${e.message}`))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
server(routes)
|
package/module.mjs
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import http from 'node:http'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2)
|
|
6
|
+
const params = {}
|
|
7
|
+
for (let i = 0; i < args.length; i++) {
|
|
8
|
+
const arg = args[i]
|
|
9
|
+
if (arg.startsWith('-')) {
|
|
10
|
+
const key = arg.slice(1)
|
|
11
|
+
const nextArg = args[i + 1]
|
|
12
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
13
|
+
params[key] = nextArg
|
|
14
|
+
i++ // Skip the next argument since we used it
|
|
15
|
+
} else {
|
|
16
|
+
params[key] = true // Boolean flag
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function public_file(r, s) {
|
|
22
|
+
if (r.url == '/') r.url = '/index.html'
|
|
23
|
+
const fn = `${params.public || './public'}${r.url.replace(/\.\./g, '')}`
|
|
24
|
+
if (fs.existsSync(fn)) {
|
|
25
|
+
const content = fs.readFileSync(fn, 'utf-8')
|
|
26
|
+
if (fn.match(/.js$/)) {
|
|
27
|
+
s.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
28
|
+
} else {
|
|
29
|
+
s.writeHead(200)
|
|
30
|
+
}
|
|
31
|
+
s.end(content)
|
|
32
|
+
return true // Indicate file was served
|
|
33
|
+
}
|
|
34
|
+
return false // Indicate no file was served
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default async function (routes, port = params.port || 3000, ip = params.ip || '127.0.0.1') {
|
|
38
|
+
const publicDir = params.public || './public'
|
|
39
|
+
if (publicDir.includes('..')) {
|
|
40
|
+
throw new Error('Public directory path cannot contain ".."')
|
|
41
|
+
}
|
|
42
|
+
if (!fs.existsSync(publicDir)) {
|
|
43
|
+
throw new Error(`Public directory "${publicDir}" does not exist`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const requestHandler = async (r, s) => {
|
|
47
|
+
let sdata = '', rrurl = r.url || ''
|
|
48
|
+
let responseSent = false
|
|
49
|
+
|
|
50
|
+
r.on('data', (s) => sdata += s.toString().trim())
|
|
51
|
+
r.on('end', (x) => {
|
|
52
|
+
try {
|
|
53
|
+
// Compose data object
|
|
54
|
+
const data = sdata ? JSON.parse(sdata) : {}
|
|
55
|
+
const qs = rrurl.split('?')
|
|
56
|
+
if(qs && qs[1]) {
|
|
57
|
+
const o = JSON.parse('{"' + decodeURI(qs[1].replace(/&/g, "\",\"").replace(/=/g, "\":\"")) + '"}')
|
|
58
|
+
Object.assign(data, o)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const midware = Object.keys(routes)
|
|
62
|
+
.filter((k) => k.startsWith('_'))
|
|
63
|
+
.find((k) => {
|
|
64
|
+
const result = routes[k](r, s, data)
|
|
65
|
+
if (result && !responseSent) {
|
|
66
|
+
responseSent = true
|
|
67
|
+
s.end(typeof result === 'string' ? result : JSON.stringify(result))
|
|
68
|
+
}
|
|
69
|
+
return result
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Response closed by middleware
|
|
73
|
+
if(responseSent || s.writableEnded) return
|
|
74
|
+
|
|
75
|
+
// Try to serve public file
|
|
76
|
+
if(public_file(r, s)) {
|
|
77
|
+
responseSent = true
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const url = rrurl.split('/')[1].split('?')[0]
|
|
82
|
+
if (routes[url]) {
|
|
83
|
+
const resp = routes[url](r, s, data)
|
|
84
|
+
if (!responseSent && !s.writableEnded) {
|
|
85
|
+
responseSent = true
|
|
86
|
+
s.end(typeof resp === 'string' ? resp:JSON.stringify(resp))
|
|
87
|
+
}
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!responseSent && !s.writableEnded) {
|
|
92
|
+
responseSent = true
|
|
93
|
+
s.writeHead(404);
|
|
94
|
+
s.end();
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error(e.stack)
|
|
98
|
+
if (!responseSent && !s.writableEnded) {
|
|
99
|
+
responseSent = true
|
|
100
|
+
s.writeHead(500).end()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let server
|
|
107
|
+
if (params.secure) {
|
|
108
|
+
const certPath = './cert.pem'
|
|
109
|
+
const keyPath = './key.pem'
|
|
110
|
+
|
|
111
|
+
if (!fs.existsSync(certPath) || !fs.existsSync(keyPath)) {
|
|
112
|
+
throw new Error('Certificate files not found. Run ./generate-certs.sh first.')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const options = {
|
|
116
|
+
cert: fs.readFileSync(certPath),
|
|
117
|
+
key: fs.readFileSync(keyPath)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
server = https.createServer(options, requestHandler)
|
|
121
|
+
} else {
|
|
122
|
+
server = http.createServer(requestHandler)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
server.listen(port || 3000, ip || '')
|
|
126
|
+
|
|
127
|
+
const protocol = params.secure ? 'https' : 'http'
|
|
128
|
+
console.log(`started on: ${protocol}://${(process.env.ip || ip)}:${(process.env.port || port)}, public: ${publicDir}, ${Object.keys(routes).length > 0 ? `using routes: ${Object.keys(routes)}` : 'not using routes'}`)
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
routes: routes,
|
|
132
|
+
port: port,
|
|
133
|
+
server: server,
|
|
134
|
+
stop: () => { server.close(); return true }
|
|
135
|
+
}
|
|
136
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "instaserve",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Instant web stack",
|
|
5
|
+
"main": "module.mjs",
|
|
6
|
+
"bin": "./instaserve",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node instaserve"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"chalk": "^5.4.1"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Instaserve</title>
|
|
5
|
+
<style>
|
|
6
|
+
:root {
|
|
7
|
+
--primary: #3b82f6;
|
|
8
|
+
--primary-dark: #2563eb;
|
|
9
|
+
--text: #1f2937;
|
|
10
|
+
--text-light: #6b7280;
|
|
11
|
+
--bg: #f9fafb;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
max-width: 800px;
|
|
17
|
+
margin: 0 auto;
|
|
18
|
+
padding: 20px 15px;
|
|
19
|
+
color: var(--text);
|
|
20
|
+
background: var(--bg);
|
|
21
|
+
}
|
|
22
|
+
.header {
|
|
23
|
+
text-align: center;
|
|
24
|
+
margin-bottom: 20px;
|
|
25
|
+
}
|
|
26
|
+
.orb {
|
|
27
|
+
width: 60px;
|
|
28
|
+
height: 60px;
|
|
29
|
+
background: radial-gradient(circle at 30% 30%, #60a5fa, var(--primary));
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
margin: 0 auto 10px;
|
|
32
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
33
|
+
}
|
|
34
|
+
h1 {
|
|
35
|
+
color: var(--primary);
|
|
36
|
+
font-size: 2rem;
|
|
37
|
+
margin: 0;
|
|
38
|
+
}
|
|
39
|
+
.subtitle {
|
|
40
|
+
color: var(--text-light);
|
|
41
|
+
font-size: 1rem;
|
|
42
|
+
margin: 5px 0;
|
|
43
|
+
}
|
|
44
|
+
h2 {
|
|
45
|
+
color: var(--text);
|
|
46
|
+
font-size: 1.3rem;
|
|
47
|
+
margin: 20px 0 10px;
|
|
48
|
+
padding-bottom: 5px;
|
|
49
|
+
border-bottom: 2px solid var(--primary);
|
|
50
|
+
}
|
|
51
|
+
h3 {
|
|
52
|
+
color: var(--text);
|
|
53
|
+
font-size: 1.1rem;
|
|
54
|
+
margin: 15px 0 8px;
|
|
55
|
+
}
|
|
56
|
+
code {
|
|
57
|
+
background: #f3f4f6;
|
|
58
|
+
padding: 2px 4px;
|
|
59
|
+
border-radius: 4px;
|
|
60
|
+
font-family: 'SF Mono', Menlo, monospace;
|
|
61
|
+
color: var(--primary-dark);
|
|
62
|
+
}
|
|
63
|
+
pre {
|
|
64
|
+
background: #f3f4f6;
|
|
65
|
+
padding: 10px;
|
|
66
|
+
border-radius: 6px;
|
|
67
|
+
overflow-x: auto;
|
|
68
|
+
border: 1px solid #e5e7eb;
|
|
69
|
+
margin: 10px 0;
|
|
70
|
+
font-size: 0.9rem;
|
|
71
|
+
}
|
|
72
|
+
.option {
|
|
73
|
+
margin: 8px 0;
|
|
74
|
+
padding-left: 0;
|
|
75
|
+
position: relative;
|
|
76
|
+
display: flex;
|
|
77
|
+
gap: 8px;
|
|
78
|
+
}
|
|
79
|
+
.option:before {
|
|
80
|
+
display: none;
|
|
81
|
+
}
|
|
82
|
+
.option code {
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
ul {
|
|
86
|
+
padding-left: 15px;
|
|
87
|
+
margin: 8px 0;
|
|
88
|
+
}
|
|
89
|
+
li {
|
|
90
|
+
margin: 5px 0;
|
|
91
|
+
}
|
|
92
|
+
.usage {
|
|
93
|
+
background: white;
|
|
94
|
+
padding: 15px;
|
|
95
|
+
border-radius: 6px;
|
|
96
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
97
|
+
margin: 15px 0;
|
|
98
|
+
}
|
|
99
|
+
p {
|
|
100
|
+
margin: 8px 0;
|
|
101
|
+
}
|
|
102
|
+
</style>
|
|
103
|
+
</head>
|
|
104
|
+
<body>
|
|
105
|
+
<div class="header">
|
|
106
|
+
<div class="orb"></div>
|
|
107
|
+
<h1>Instaserve</h1>
|
|
108
|
+
<p class="subtitle">Instant web stack for Node.js</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="usage">
|
|
112
|
+
<h2>Usage</h2>
|
|
113
|
+
<pre><code>npx instaserve [options]</code></pre>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<h2>Options</h2>
|
|
117
|
+
<div class="option"><code>-port <number></code> - Port to listen on (default: 3000)</div>
|
|
118
|
+
<div class="option"><code>-ip <address></code> - IP address to bind to (default: 127.0.0.1)</div>
|
|
119
|
+
<div class="option"><code>-public <path></code> - Public directory path (default: ./public)</div>
|
|
120
|
+
<div class="option"><code>-api <file></code> - Path to routes file (default: ./routes.js)</div>
|
|
121
|
+
<div class="option"><code>-secure</code> - Enable HTTPS (requires cert.pem and key.pem - run ./generate-certs.sh)</div>
|
|
122
|
+
<div class="option"><code>-help</code> - Show help message</div>
|
|
123
|
+
|
|
124
|
+
<h2>HTTPS Support</h2>
|
|
125
|
+
<p>Instaserve supports HTTPS with self-signed certificates. To enable HTTPS:</p>
|
|
126
|
+
<ol>
|
|
127
|
+
<li><strong>Generate certificates:</strong> <code>./generate-certs.sh</code></li>
|
|
128
|
+
<li><strong>Run with HTTPS:</strong> <code>npx instaserve -secure</code></li>
|
|
129
|
+
</ol>
|
|
130
|
+
<p>The certificate generation script creates trusted certificates that won't show browser warnings.</p>
|
|
131
|
+
|
|
132
|
+
<h2>Routes</h2>
|
|
133
|
+
<p>The routes file (<code>routes.js</code> by default) defines your API endpoints. Each route is a function that handles requests to a specific URL path.</p>
|
|
134
|
+
|
|
135
|
+
<h3>Basic Route Example</h3>
|
|
136
|
+
<pre><code>export default {
|
|
137
|
+
// Handle GET /hello
|
|
138
|
+
hello: (req, res, data) => {
|
|
139
|
+
return { message: 'Hello World' }
|
|
140
|
+
}
|
|
141
|
+
}</code></pre>
|
|
142
|
+
|
|
143
|
+
<h3>Special Routes (Middleware)</h3>
|
|
144
|
+
<p>Routes starting with <code>_</code> are middleware functions that run on <strong>every request</strong> before the main route handler. They are useful for:</p>
|
|
145
|
+
<ul>
|
|
146
|
+
<li>Logging requests</li>
|
|
147
|
+
<li>Authentication</li>
|
|
148
|
+
<li>Request modification</li>
|
|
149
|
+
<li>Response headers</li>
|
|
150
|
+
</ul>
|
|
151
|
+
<p>Middleware functions can:</p>
|
|
152
|
+
<ul>
|
|
153
|
+
<li>Return <code>false</code> to continue to the next middleware or main route</li>
|
|
154
|
+
<li>Return a truthy value to stop processing and use that as the response</li>
|
|
155
|
+
<li>Modify the request or response objects</li>
|
|
156
|
+
</ul>
|
|
157
|
+
|
|
158
|
+
<h4>Middleware Example</h4>
|
|
159
|
+
<pre><code>export default {
|
|
160
|
+
// Log every request
|
|
161
|
+
_log: (req, res, data) => {
|
|
162
|
+
console.log(`${req.method} ${req.url}`)
|
|
163
|
+
return false // Continue processing
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Block unauthorized requests
|
|
167
|
+
_auth: (req, res, data) => {
|
|
168
|
+
if (!data.token) {
|
|
169
|
+
res.writeHead(401)
|
|
170
|
+
return 'Unauthorized'
|
|
171
|
+
}
|
|
172
|
+
return false // Continue if authorized
|
|
173
|
+
}
|
|
174
|
+
}</code></pre>
|
|
175
|
+
|
|
176
|
+
<h3>Route Parameters</h3>
|
|
177
|
+
<p>Each route function receives:</p>
|
|
178
|
+
<ul>
|
|
179
|
+
<li><code>req</code> - The HTTP request object</li>
|
|
180
|
+
<li><code>res</code> - The HTTP response object</li>
|
|
181
|
+
<li><code>data</code> - Combined data from:
|
|
182
|
+
<ul>
|
|
183
|
+
<li>POST body (if JSON)</li>
|
|
184
|
+
<li>URL query parameters</li>
|
|
185
|
+
<li>Form data</li>
|
|
186
|
+
</ul>
|
|
187
|
+
</li>
|
|
188
|
+
</ul>
|
|
189
|
+
|
|
190
|
+
<h3>Example Routes File</h3>
|
|
191
|
+
<pre><code>export default {
|
|
192
|
+
// Middleware example
|
|
193
|
+
_debug: (req, res, data) => {
|
|
194
|
+
console.log('Request:', req.url)
|
|
195
|
+
return false // Continue to next route
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// API endpoint
|
|
199
|
+
api: (req, res, data) => {
|
|
200
|
+
return { status: 'ok', data }
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// Error handling
|
|
204
|
+
testerror: () => {
|
|
205
|
+
throw new Error('Test error')
|
|
206
|
+
}
|
|
207
|
+
}</code></pre>
|
|
208
|
+
</body>
|
|
209
|
+
</html>
|
package/routes.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file defines the API routes and middleware for the Instaserve web server.
|
|
3
|
+
*
|
|
4
|
+
* The file exports a default object containing route handlers and middleware functions.
|
|
5
|
+
*
|
|
6
|
+
* Middleware functions (prefixed with _):
|
|
7
|
+
* - _debug: Logs request method, URL and data to console
|
|
8
|
+
* - _returnfalsy: Example middleware that returns true to stop request processing
|
|
9
|
+
* - _example: Demonstrates middleware behavior with console log
|
|
10
|
+
* - _end: Example of early response termination (commented out)
|
|
11
|
+
*
|
|
12
|
+
* Route handlers:
|
|
13
|
+
* - api: Returns a string response with the request data
|
|
14
|
+
* - testerror: Throws an error for testing error handling
|
|
15
|
+
* - testdata: Example route that returns the context parameter
|
|
16
|
+
*
|
|
17
|
+
* Each route handler receives:
|
|
18
|
+
* - req: HTTP request object
|
|
19
|
+
* - s: HTTP response object
|
|
20
|
+
* - data: Combined request data (POST body, query params, form data)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
// Middleware functions (prefixed with _) run on every request
|
|
25
|
+
// Return false to continue processing, or a value to use as response
|
|
26
|
+
|
|
27
|
+
// Example: Log all requests
|
|
28
|
+
_log: (req, res, data) => {
|
|
29
|
+
console.log(`${req.method} ${req.url}`)
|
|
30
|
+
return false // Continue to next middleware or route
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**** Example: Basic authentication
|
|
34
|
+
_auth: (req, res, data) => {
|
|
35
|
+
if (!data.token) {
|
|
36
|
+
res.writeHead(401)
|
|
37
|
+
return 'Unauthorized'
|
|
38
|
+
}
|
|
39
|
+
return false // Continue if authorized
|
|
40
|
+
}, */
|
|
41
|
+
|
|
42
|
+
// Regular route handlers
|
|
43
|
+
hello: (req, res, data) => {
|
|
44
|
+
return { message: 'Hello World' }
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
_debug: ({method, url}, s, data) => { console.log(method, url, data) },
|
|
48
|
+
_example: (r, s) => console.log('in routes.mjs, returning a truthy value (above) will stop the chain'),
|
|
49
|
+
// _returnfalsy: (r, s) => { return true }, // Commented out - interferes with static file serving
|
|
50
|
+
|
|
51
|
+
api: (r, s, data) => 'an example api response, data:' + JSON.stringify(data),
|
|
52
|
+
testerror: () => { throw new Error('this from testerror') },
|
|
53
|
+
testdata: (r, s, c, d) => c
|
|
54
|
+
}
|
package/index.mjs
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import http from 'node:http'
|
|
3
|
-
import fs from 'node:fs'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
import { resolve } from 'node:path'
|
|
6
|
-
|
|
7
|
-
const routesfile = resolve('routes.mjs')
|
|
8
|
-
const routesurl = pathToFileURL(routesfile).href
|
|
9
|
-
console.log(routesfile, routesurl)
|
|
10
|
-
|
|
11
|
-
if (!fs.existsSync(routesfile)) {
|
|
12
|
-
fs.writeFileSync(routesfile, `export default {
|
|
13
|
-
_debug: ({ r, s, db }) => console.log(r.url, r.method),
|
|
14
|
-
'/': ({ s }) => s.endJSON({ message: 'hello index' }),
|
|
15
|
-
'tables': name => ([k, v]) => k.match(name + ':')
|
|
16
|
-
}`)
|
|
17
|
-
}
|
|
18
|
-
const routes = (await import(routesurl)).default
|
|
19
|
-
routes['/sw.js'] = e => {
|
|
20
|
-
e.s.writeHead(201, { 'Content-Type': 'application/javascript' })
|
|
21
|
-
e.s.end('self.addEventListener("fetch",e=>{e.respondWith(caches.open("e").then(function(t){return t.match(e.request).then(function(n){return n||fetch(e.request).then(function(n){return e.request.url.match(/\.js$|\.css$/)&&t.put(e.request,n.clone()),n})})}))});')
|
|
22
|
-
}
|
|
23
|
-
const VastDB = (await import(`./vastdb.mjs`)).default
|
|
24
|
-
const db = new VastDB(routes)
|
|
25
|
-
console.log(db.filename, routes)
|
|
26
|
-
|
|
27
|
-
http
|
|
28
|
-
.createServer(async (r, s) => {
|
|
29
|
-
try {
|
|
30
|
-
let data = '';
|
|
31
|
-
r.on('data', (s) => (data += s.toString()));
|
|
32
|
-
r.on('end', (x) => {
|
|
33
|
-
try {
|
|
34
|
-
data = JSON.parse(data);
|
|
35
|
-
} catch { }
|
|
36
|
-
});
|
|
37
|
-
s.endJSON = o => s.end(JSON.stringify(o))
|
|
38
|
-
const midware = Object.keys(routes)
|
|
39
|
-
.filter((k) => k.startsWith('_'))
|
|
40
|
-
.map((k) => routes[k]({ r, s, data, db }));
|
|
41
|
-
if (midware.includes(true)) return;
|
|
42
|
-
if (routes[r.url]) return routes[r.url]({ r, s, data, db });
|
|
43
|
-
else s.writeHead(404).end()
|
|
44
|
-
} catch (e) {
|
|
45
|
-
console.log(e);
|
|
46
|
-
s.writeHead(404).end();
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
.listen(3000, (x) => console.log('listening on 3000'));
|