instaserve 1.1.8 → 1.1.10
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/instaserve +61 -47
- package/lib/auth0.js +45 -0
- package/lib/r2.js +217 -0
- package/lib/sqlite.js +39 -0
- package/module.mjs +3 -1
- package/package.json +4 -3
- package/test.js +0 -28
package/instaserve
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import fs from 'node:fs'
|
|
5
5
|
import path from 'node:path'
|
|
6
|
-
import { pathToFileURL } from 'node:url'
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from 'node:url'
|
|
7
7
|
|
|
8
8
|
console.log(chalk.cyan('\nInstaserve - Instant Web Stack\n'))
|
|
9
9
|
console.log(chalk.yellow('Usage:'))
|
|
@@ -20,20 +20,20 @@ console.log(chalk.green(' -secure') + ' Enable HTTPS (requires cert.p
|
|
|
20
20
|
console.log(chalk.green(' -help') + ' Show this help message\n')
|
|
21
21
|
|
|
22
22
|
if (process.argv.includes('-help')) {
|
|
23
|
-
|
|
23
|
+
process.exit(0)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const args = process.argv.slice(2)
|
|
27
27
|
|
|
28
28
|
// Handle generate-routes command
|
|
29
29
|
if (args[0] === 'generate-routes') {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
const routesFile = path.resolve(process.cwd(), './routes.js')
|
|
31
|
+
if (fs.existsSync(routesFile)) {
|
|
32
|
+
console.error(chalk.red(`Error: routes.js already exists`))
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sampleRoutes = `export default {
|
|
37
37
|
// Middleware functions (prefixed with _) run on every request
|
|
38
38
|
// Return false to continue processing, or a value to use as response
|
|
39
39
|
|
|
@@ -71,27 +71,41 @@ if (args[0] === 'generate-routes') {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
`
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
|
|
75
|
+
fs.writeFileSync(routesFile, sampleRoutes)
|
|
76
|
+
console.log(chalk.green(`✓ Created routes.js`))
|
|
77
|
+
|
|
78
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
79
|
+
const sourceLib = path.join(__dirname, 'lib')
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(sourceLib)) {
|
|
82
|
+
const destLib = path.join(process.cwd(), 'lib')
|
|
83
|
+
if (!fs.existsSync(destLib)) {
|
|
84
|
+
fs.cpSync(sourceLib, destLib, { recursive: true })
|
|
85
|
+
console.log(chalk.green(`✓ Created lib/`))
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.yellow(`! lib/ directory already exists, skipping copy`))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
process.exit(0)
|
|
78
92
|
}
|
|
79
93
|
|
|
80
94
|
import server from './module.mjs'
|
|
81
95
|
|
|
82
96
|
const params = {}
|
|
83
97
|
for (let i = 0; i < args.length; i++) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
98
|
+
const arg = args[i]
|
|
99
|
+
if (arg.startsWith('-')) {
|
|
100
|
+
const key = arg.slice(1)
|
|
101
|
+
const nextArg = args[i + 1]
|
|
102
|
+
if (nextArg && !nextArg.startsWith('-')) {
|
|
103
|
+
params[key] = nextArg
|
|
104
|
+
i++ // Skip the next argument since we used it
|
|
105
|
+
} else {
|
|
106
|
+
params[key] = true // Boolean flag
|
|
94
107
|
}
|
|
108
|
+
}
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
// Load routes file
|
|
@@ -101,37 +115,37 @@ const routesFileParam = params.api || './routes.js'
|
|
|
101
115
|
const routesFileSpecified = !!params.api
|
|
102
116
|
|
|
103
117
|
// Resolve to absolute path from current working directory
|
|
104
|
-
const routesFile = path.isAbsolute(routesFileParam)
|
|
105
|
-
|
|
106
|
-
|
|
118
|
+
const routesFile = path.isAbsolute(routesFileParam)
|
|
119
|
+
? routesFileParam
|
|
120
|
+
: path.resolve(process.cwd(), routesFileParam)
|
|
107
121
|
|
|
108
122
|
if (routesFileSpecified && !fs.existsSync(routesFile)) {
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
console.error(chalk.red(`Error: Routes file "${routesFileParam}" does not exist`))
|
|
124
|
+
process.exit(1)
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
if (fs.existsSync(routesFile)) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
process.exit(1)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
routesFilePath = routesFile
|
|
131
|
-
} catch (e) {
|
|
132
|
-
console.error(chalk.red(`Error: Could not load routes file "${routesFileParam}": ${e.message}`))
|
|
128
|
+
try {
|
|
129
|
+
const routesFileURL = pathToFileURL(routesFile).href
|
|
130
|
+
const imported = await import(routesFileURL)
|
|
131
|
+
routes = imported.default || imported
|
|
132
|
+
|
|
133
|
+
if (!routes || typeof routes !== 'object' || Array.isArray(routes)) {
|
|
134
|
+
console.error(chalk.red(`Error: Routes file "${routesFileParam}" must export a default object`))
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const [key, handler] of Object.entries(routes)) {
|
|
139
|
+
if (typeof handler !== 'function') {
|
|
140
|
+
console.error(chalk.red(`Error: Route "${key}" in "${routesFileParam}" must be a function`))
|
|
133
141
|
process.exit(1)
|
|
142
|
+
}
|
|
134
143
|
}
|
|
144
|
+
routesFilePath = routesFile
|
|
145
|
+
} catch (e) {
|
|
146
|
+
console.error(chalk.red(`Error: Could not load routes file "${routesFileParam}": ${e.message}`))
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
server(routes, params.port ? parseInt(params.port) : undefined, params.ip, routesFilePath)
|
package/lib/auth0.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"GET /login": (req, res) => {
|
|
3
|
+
const REDIRECT_URI = "https://${req.headers.host}/callback";
|
|
4
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
5
|
+
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)}`;
|
|
7
|
+
res.writeHead(302, { Location: url });
|
|
8
|
+
res.end();
|
|
9
|
+
},
|
|
10
|
+
"GET /logout": (req, res, data) => {
|
|
11
|
+
const token = req.headers.cookie?.match(/token=([^;]+)/)?.[1];
|
|
12
|
+
console.log(`logout: ${token}`);
|
|
13
|
+
if (token) {
|
|
14
|
+
global.sqlite.prepare("UPDATE users SET token = NULL WHERE token = ?").run(token);
|
|
15
|
+
}
|
|
16
|
+
res.setHeader("Set-Cookie", "token=; Path=/; HttpOnly; Secure; SameSite=Lax");
|
|
17
|
+
res.writeHead(302, { Location: "/" });
|
|
18
|
+
res.end();
|
|
19
|
+
},
|
|
20
|
+
"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", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "content-type": "application/json" },
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
grant_type: "authorization_code",
|
|
27
|
+
client_id: secrets.auth0_clientid,
|
|
28
|
+
client_secret: secrets.auth0_clientsecret,
|
|
29
|
+
code: data.code,
|
|
30
|
+
redirect_uri: REDIRECT_URI
|
|
31
|
+
}),
|
|
32
|
+
})
|
|
33
|
+
const tokens = await tokenRes.json();
|
|
34
|
+
// decrypt token
|
|
35
|
+
const id_token = tokens.id_token;
|
|
36
|
+
const payload = JSON.parse(Buffer.from(id_token.split('.')[1], 'base64').toString());
|
|
37
|
+
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 = ?")
|
|
39
|
+
.run(payload.email, payload.name, auth_token, auth_token, payload.name, payload.picture);
|
|
40
|
+
res.setHeader("Set-Cookie", `username=${payload.email}; Path=/;`);
|
|
41
|
+
res.setHeader("Set-Cookie", `name=${payload.name}; Path=/;`);
|
|
42
|
+
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>`;
|
|
44
|
+
}
|
|
45
|
+
}
|
package/lib/r2.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
|
|
5
|
+
// --- Core SigV4 Utilities ---
|
|
6
|
+
|
|
7
|
+
const hashSHA256 = (str) => crypto.createHash('sha256').update(str).digest('hex');
|
|
8
|
+
const hmacSHA256 = (key, str) => crypto.createHmac('sha256', key).update(str).digest();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* FINAL FIX: Strict S3 encoding for the Object Key (Canonical URI Path).
|
|
12
|
+
* This function encodes all necessary characters (including spaces as %20)
|
|
13
|
+
* but preserves the path separator ('/'). It also ensures hex codes are uppercase,
|
|
14
|
+
* which is critical for signature matching in some S3 implementations.
|
|
15
|
+
*/
|
|
16
|
+
const encodePath = (path) => {
|
|
17
|
+
// 1. Encode all path components fully
|
|
18
|
+
const segments = path.split('/').map(segment => encodeURIComponent(segment));
|
|
19
|
+
|
|
20
|
+
// 2. Join the segments back with unencoded slashes
|
|
21
|
+
const encodedPath = segments.join('/');
|
|
22
|
+
|
|
23
|
+
// 3. Normalize: replace common encoding issues with uppercase hex for consistency
|
|
24
|
+
// S3 requires these replacements for canonical request:
|
|
25
|
+
return encodedPath.replace(/[!'()*]/g, function (c) {
|
|
26
|
+
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates SigV4 signed request components for Cloudflare R2 (S3-compatible).
|
|
33
|
+
*/
|
|
34
|
+
function signR2(method, bucket, key, body, accessKeyId, secretAccessKey, accountId, queryString = '') {
|
|
35
|
+
const service = 's3', region = 'auto';
|
|
36
|
+
const now = new Date();
|
|
37
|
+
const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
|
|
38
|
+
|
|
39
|
+
// Date format (YYYYMMDDTHHMMSSZ)
|
|
40
|
+
const amzDate = now.toISOString().substring(0, 19).replace(/[:-]/g, '') + 'Z';
|
|
41
|
+
|
|
42
|
+
// 1. Canonical URI - Uses the final, strict encodePath
|
|
43
|
+
const objectPath = key ? encodePath(key) : '';
|
|
44
|
+
const canonicalUri = `/${bucket}${objectPath ? '/' + objectPath : ''}`;
|
|
45
|
+
|
|
46
|
+
// 2. Canonical Query String (Encoded, Sorted)
|
|
47
|
+
const canonicalQuerystring = queryString ? queryString.split('&')
|
|
48
|
+
.map(p => {
|
|
49
|
+
const [k, v = ''] = p.split('=');
|
|
50
|
+
// Decode then re-encode to prevent double encoding
|
|
51
|
+
return {
|
|
52
|
+
k: encodeURIComponent(decodeURIComponent(k)),
|
|
53
|
+
v: encodeURIComponent(decodeURIComponent(v))
|
|
54
|
+
};
|
|
55
|
+
})
|
|
56
|
+
.sort((a, b) => a.k.localeCompare(b.k) || a.v.localeCompare(b.v))
|
|
57
|
+
.map(p => `${p.k}=${p.v}`).join('&') : '';
|
|
58
|
+
|
|
59
|
+
// 3. Headers & Payload
|
|
60
|
+
const host = `${accountId}.r2.cloudflarestorage.com`;
|
|
61
|
+
const payloadHash = ['GET', 'DELETE'].includes(method) ? 'UNSIGNED-PAYLOAD' : hashSHA256(body ?? '');
|
|
62
|
+
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
|
|
63
|
+
const canonicalHeaders = `host:${host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n`;
|
|
64
|
+
|
|
65
|
+
// 4. Canonical Request
|
|
66
|
+
const canonicalRequest = [method, canonicalUri, canonicalQuerystring, canonicalHeaders, signedHeaders, payloadHash].join('\n');
|
|
67
|
+
|
|
68
|
+
// 5. String to Sign & 6. Signature (Key Derivation)
|
|
69
|
+
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`;
|
|
70
|
+
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, credentialScope, hashSHA256(canonicalRequest)].join('\n');
|
|
71
|
+
|
|
72
|
+
// Key derivation condensed
|
|
73
|
+
const kSigning = hmacSHA256(
|
|
74
|
+
hmacSHA256(hmacSHA256(hmacSHA256(Buffer.from('AWS4' + secretAccessKey, 'utf8'), dateStamp), region), service),
|
|
75
|
+
'aws4_request'
|
|
76
|
+
);
|
|
77
|
+
const signature = crypto.createHmac('sha256', kSigning).update(stringToSign).digest('hex');
|
|
78
|
+
|
|
79
|
+
// 7. Authorization Header & Final Request Prep
|
|
80
|
+
const authorization = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
81
|
+
|
|
82
|
+
// The URL must also use the same, correctly encoded objectPath
|
|
83
|
+
const url = `https://${host}${canonicalUri}${canonicalQuerystring ? '?' + canonicalQuerystring : ''}`;
|
|
84
|
+
|
|
85
|
+
const headers = {
|
|
86
|
+
Host: host, 'x-amz-date': amzDate, 'x-amz-content-sha256': payloadHash, Authorization: authorization
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (body !== null && typeof body !== 'undefined') {
|
|
90
|
+
headers['Content-Length'] = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { url, headers, body, method };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---- R2 request helper ----
|
|
97
|
+
function requestR2({ method, url, headers, body }) {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const { hostname, pathname, search } = new URL(url);
|
|
100
|
+
const req = https.request({ hostname, port: 443, path: pathname + (search || ''), method, headers }, res => {
|
|
101
|
+
const chunks = [];
|
|
102
|
+
res.on('data', c => chunks.push(c));
|
|
103
|
+
res.on('end', () => {
|
|
104
|
+
const data = Buffer.concat(chunks).toString();
|
|
105
|
+
if (res.statusCode >= 200 && res.statusCode < 308) {
|
|
106
|
+
resolve(data);
|
|
107
|
+
} else {
|
|
108
|
+
reject(new Error(`R2 request failed: Status ${res.statusCode} ${res.statusMessage}. Body: ${data.substring(0, 300)}`));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
req.on('error', reject);
|
|
113
|
+
if (body) req.write(body);
|
|
114
|
+
req.end();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- API Helpers ---
|
|
119
|
+
|
|
120
|
+
const getSecrets = ({ cloudflareAccessKeyId, cloudflareSecretAccessKey, cloudflareAccountId }) => (
|
|
121
|
+
[cloudflareAccessKeyId, cloudflareSecretAccessKey, cloudflareAccountId]
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
async function uploadToR2(bucket, key, body, contentType, secrets) {
|
|
125
|
+
if (!body) body = Buffer.alloc(0);
|
|
126
|
+
const signed = signR2('PUT', bucket, key, body, ...getSecrets(secrets));
|
|
127
|
+
signed.headers['Content-Type'] = contentType || 'application/octet-stream';
|
|
128
|
+
await requestR2(signed);
|
|
129
|
+
return { success: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function downloadFromR2(bucket, key, secrets) {
|
|
133
|
+
const signed = signR2('GET', bucket, key, null, ...getSecrets(secrets));
|
|
134
|
+
const { hostname, pathname, search } = new URL(signed.url);
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const req = https.request({
|
|
138
|
+
hostname,
|
|
139
|
+
port: 443,
|
|
140
|
+
path: pathname + (search || ''),
|
|
141
|
+
method: 'GET',
|
|
142
|
+
headers: signed.headers
|
|
143
|
+
}, res => {
|
|
144
|
+
if (res.statusCode >= 200 && res.statusCode < 308) {
|
|
145
|
+
resolve(res);
|
|
146
|
+
} else {
|
|
147
|
+
let errorData = '';
|
|
148
|
+
res.on('data', chunk => errorData += chunk);
|
|
149
|
+
res.on('end', () => {
|
|
150
|
+
reject(new Error(`Download failed: ${res.statusCode} ${errorData.substring(0, 200)}`));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
req.on('error', reject);
|
|
155
|
+
req.end();
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function deleteFromR2(bucket, key, secrets) {
|
|
160
|
+
await requestR2(signR2('DELETE', bucket, key, null, ...getSecrets(secrets)));
|
|
161
|
+
return { success: true };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function listR2Files(bucket, prefix, secrets) {
|
|
165
|
+
const qs = prefix ? `list-type=2&prefix=${encodeURIComponent(prefix)}` : 'list-type=2';
|
|
166
|
+
|
|
167
|
+
const data = await requestR2(signR2('GET', bucket, '', null, ...getSecrets(secrets), qs));
|
|
168
|
+
|
|
169
|
+
const keys = [...data.matchAll(/<Key>(.*?)<\/Key>/g)].map(([, k]) => k);
|
|
170
|
+
const sizes = [...data.matchAll(/<Size>(\d+)<\/Size>/g)].map(([, s]) => parseInt(s, 10));
|
|
171
|
+
const mods = [...data.matchAll(/<LastModified>(.*?)<\/LastModified>/g)].map(([, m]) => m);
|
|
172
|
+
|
|
173
|
+
return keys.map((k, i) => ({ key: k, size: sizes[i] || 0, lastModified: mods[i] || '' }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getSignedDownloadUrl(bucket, key, secrets) {
|
|
177
|
+
return signR2('GET', bucket, key, null, ...getSecrets(secrets)).url;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export const fileRoutes = {
|
|
181
|
+
upload: async (req, res, data) => {
|
|
182
|
+
try {
|
|
183
|
+
let body = data.body;
|
|
184
|
+
if (typeof body === 'string') {
|
|
185
|
+
body = Buffer.from(body, 'base64');
|
|
186
|
+
} else if (body) {
|
|
187
|
+
body = Buffer.from(body);
|
|
188
|
+
} else {
|
|
189
|
+
body = Buffer.alloc(0);
|
|
190
|
+
}
|
|
191
|
+
return await uploadToR2(data.bucket, data.key, body, data.contentType, data.secrets);
|
|
192
|
+
} catch (e) { return { error: e.message }; }
|
|
193
|
+
},
|
|
194
|
+
download: async (req, res, data) => {
|
|
195
|
+
try {
|
|
196
|
+
return await downloadFromR2(data.bucket, data.key, data.secrets);
|
|
197
|
+
} catch (e) { return { error: e.message }; }
|
|
198
|
+
},
|
|
199
|
+
delete: async (req, res, data) => {
|
|
200
|
+
try {
|
|
201
|
+
return await deleteFromR2(data.bucket, data.key, data.secrets);
|
|
202
|
+
} catch (e) { return { error: e.message }; }
|
|
203
|
+
},
|
|
204
|
+
"POST /files": async (req, res, data) => {
|
|
205
|
+
try {
|
|
206
|
+
return await listR2Files(data.bucket, data.prefix, data.secrets);
|
|
207
|
+
} catch (e) { return { error: e.message }; }
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export {
|
|
212
|
+
uploadToR2,
|
|
213
|
+
downloadFromR2,
|
|
214
|
+
deleteFromR2,
|
|
215
|
+
listR2Files,
|
|
216
|
+
getSignedDownloadUrl
|
|
217
|
+
};
|
package/lib/sqlite.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
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
|
+
|
|
6
|
+
// Initialize database
|
|
7
|
+
global.sqlite = new Database("data.db");
|
|
8
|
+
sqlite.prepare("CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, username TEXT, pass TEXT, token TEXT, picture TEXT, name TEXT, email TEXT)").run();
|
|
9
|
+
sqlite.prepare("CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT)").run();
|
|
10
|
+
|
|
11
|
+
// Combine all routes
|
|
12
|
+
export default {
|
|
13
|
+
_auth: (req, res, data) => {
|
|
14
|
+
if (req.url.match(/js|html|css/)) return;
|
|
15
|
+
if (req.url == "/") return 401;
|
|
16
|
+
if (req.url.startsWith("/register") || req.url.startsWith("/login")) return;
|
|
17
|
+
|
|
18
|
+
const token = req.headers.cookie?.split('token=')[1].split(';')[0];
|
|
19
|
+
if (!token) { return 401; }
|
|
20
|
+
const user = sqlite.prepare("SELECT * FROM users WHERE token = ?").get(token);
|
|
21
|
+
if (!user) return 401;
|
|
22
|
+
req.user = user.username;
|
|
23
|
+
},
|
|
24
|
+
"POST /api": (req, res, data) => {
|
|
25
|
+
sqlite.prepare("INSERT OR REPLACE INTO kv (key, value) VALUES (?, ?)").run(req.user + ":" + data.key, data.value);
|
|
26
|
+
return sqlite.prepare("SELECT substr(key, instr(key, ':') + 1) AS key, value FROM kv WHERE key = ?").get(req.user + ":" + data.key);
|
|
27
|
+
},
|
|
28
|
+
"GET /api": (req, res, data) => {
|
|
29
|
+
return sqlite.prepare("SELECT substr(key, instr(key, ':') + 1) AS key, value FROM kv WHERE key = ?").get(req.user + ":" + data.key) || {};
|
|
30
|
+
},
|
|
31
|
+
"GET /all": (req, res, data) => {
|
|
32
|
+
const rows = sqlite.prepare("SELECT substr(key, instr(key, ':') + 1) AS key, value FROM kv WHERE key LIKE ?").all(req.user + ":%");
|
|
33
|
+
return rows || [];
|
|
34
|
+
},
|
|
35
|
+
"DELETE /api": (req, res, data) => {
|
|
36
|
+
sqlite.prepare("DELETE FROM kv WHERE key = ?").run(req.user + ":" + data.key);
|
|
37
|
+
return { deleted: true, key: data.key };
|
|
38
|
+
}
|
|
39
|
+
};
|
package/module.mjs
CHANGED
|
@@ -62,7 +62,9 @@ export default async function (routes, port = params.port || 3000, ip = params.i
|
|
|
62
62
|
s.writeHead(result)
|
|
63
63
|
s.end()
|
|
64
64
|
} else {
|
|
65
|
-
|
|
65
|
+
if (result != "SSE") {
|
|
66
|
+
s.end(typeof result === 'string' ? result : JSON.stringify(result))
|
|
67
|
+
}
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instaserve",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
4
4
|
"description": "Instant web stack",
|
|
5
5
|
"main": "module.mjs",
|
|
6
6
|
"bin": "./instaserve",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"chalk": "^5.6.2"
|
|
12
|
+
"chalk": "^5.6.2",
|
|
13
|
+
"instaserve": "^1.1.9"
|
|
13
14
|
}
|
|
14
|
-
}
|
|
15
|
+
}
|
package/test.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
// Middleware functions (prefixed with _) run on every request
|
|
3
|
-
// Return false to continue processing, or a value to use as response
|
|
4
|
-
|
|
5
|
-
// Example: Log all requests
|
|
6
|
-
_log: (req, res, data) => {
|
|
7
|
-
console.log(`${req.method} ${req.url}`)
|
|
8
|
-
return false // Continue to next middleware or route
|
|
9
|
-
},
|
|
10
|
-
|
|
11
|
-
// Example: Basic authentication (commented out)
|
|
12
|
-
// _auth: (req, res, data) => {
|
|
13
|
-
// if (!data.token) {
|
|
14
|
-
// res.writeHead(401)
|
|
15
|
-
// return 'Unauthorized'
|
|
16
|
-
// }
|
|
17
|
-
// return false // Continue if authorized
|
|
18
|
-
// },
|
|
19
|
-
|
|
20
|
-
// Regular route handlers
|
|
21
|
-
hello: (req, res, data) => {
|
|
22
|
-
return { message: 'Hello World' }
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
api: (req, res, data) => {
|
|
26
|
-
return { message: 'API endpoint', data }
|
|
27
|
-
}
|
|
28
|
-
}
|