instaserve 1.1.9 → 1.1.11

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 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
- process.exit(0)
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
- 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 {
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
- fs.writeFileSync(routesFile, sampleRoutes)
76
- console.log(chalk.green(`✓ Created routes.js`))
77
- process.exit(0)
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
- const arg = args[i]
85
- if (arg.startsWith('-')) {
86
- const key = arg.slice(1)
87
- const nextArg = args[i + 1]
88
- if (nextArg && !nextArg.startsWith('-')) {
89
- params[key] = nextArg
90
- i++ // Skip the next argument since we used it
91
- } else {
92
- params[key] = true // Boolean flag
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
- ? routesFileParam
106
- : path.resolve(process.cwd(), routesFileParam)
118
+ const routesFile = path.isAbsolute(routesFileParam)
119
+ ? routesFileParam
120
+ : path.resolve(process.cwd(), routesFileParam)
107
121
 
108
122
  if (routesFileSpecified && !fs.existsSync(routesFile)) {
109
- console.error(chalk.red(`Error: Routes file "${routesFileParam}" does not exist`))
110
- process.exit(1)
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
- try {
115
- const routesFileURL = pathToFileURL(routesFile).href
116
- const imported = await import(routesFileURL)
117
- routes = imported.default || imported
118
-
119
- if (!routes || typeof routes !== 'object' || Array.isArray(routes)) {
120
- console.error(chalk.red(`Error: Routes file "${routesFileParam}" must export a default object`))
121
- process.exit(1)
122
- }
123
-
124
- for (const [key, handler] of Object.entries(routes)) {
125
- if (typeof handler !== 'function') {
126
- console.error(chalk.red(`Error: Route "${key}" in "${routesFileParam}" must be a function`))
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
@@ -43,9 +43,7 @@ export default async function (routes, port = params.port || 3000, ip = params.i
43
43
  if (publicDir.includes('..')) {
44
44
  throw new Error('Public directory path cannot contain ".."')
45
45
  }
46
- if (!fs.existsSync(publicDir)) {
47
- throw new Error(`Public directory "${publicDir}" does not exist`)
48
- }
46
+
49
47
 
50
48
  const requestHandler = async (r, s) => {
51
49
  let sdata = '', rrurl = r.url || ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instaserve",
3
- "version": "1.1.9",
3
+ "version": "1.1.11",
4
4
  "description": "Instant web stack",
5
5
  "main": "module.mjs",
6
6
  "bin": "./instaserve",