maxserver 1.0.1 → 2.1.0

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/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Max Matinpalo
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
package/bin/init.js DELETED
@@ -1,86 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "fs";
4
- import path from "path";
5
- import { fileURLToPath } from "url";
6
- import { execSync } from "child_process";
7
-
8
-
9
- function main() {
10
- const { projectName, targetDir, templateDir } = resolveArgs();
11
-
12
- if (fs.existsSync(targetDir)) {
13
- console.error(`❌ Error: Directory "${projectName}" already exists.`);
14
- process.exit(1);
15
- }
16
-
17
- try {
18
- console.log(`🚀 Setting up "${projectName}"`);
19
- fs.cpSync(templateDir, targetDir, { recursive: true });
20
-
21
- fixDotfiles(targetDir);
22
- patchPackageJson(targetDir, projectName);
23
-
24
- process.chdir(targetDir);
25
- console.log("📦 Installing maxserver");
26
- execSync("npm install maxserver@latest", { stdio: "inherit" });
27
-
28
- console.log(`\n✅ Install complete\n\n->\n\tcd ${projectName}\n\tnpm run dev\n`);
29
- } catch (err) {
30
- console.error("❌ Init failed:", err.message);
31
- process.exit(1);
32
- }
33
- }
34
-
35
-
36
- function resolveArgs() {
37
- const base = path.dirname(fileURLToPath(import.meta.url));
38
- const name = process.argv[2] || "maxserver";
39
-
40
- return {
41
- projectName: name,
42
- targetDir: path.resolve(process.cwd(), name),
43
- templateDir: path.resolve(base, "../templates")
44
- };
45
- }
46
-
47
-
48
- /**
49
- * Renames template files to dotfiles and clones vscode settings.
50
- */
51
- function fixDotfiles(dir) {
52
- // 1. Standard dotfile renames
53
- for (const f of ["env", "gitignore"]) {
54
- const src = path.join(dir, f);
55
- if (fs.existsSync(src)) fs.renameSync(src, path.join(dir, "." + f));
56
- }
57
-
58
- // 2. Specialized vscode handling (Root and src)
59
- const vscodeSrc = path.join(dir, "vscode");
60
-
61
- if (fs.existsSync(vscodeSrc)) {
62
- const srcDir = path.join(dir, "src");
63
-
64
- // Ensure src/ exists before cloning settings into it
65
- if (!fs.existsSync(srcDir)) fs.mkdirSync(srcDir);
66
-
67
- // Copy to src/.vscode
68
- fs.cpSync(vscodeSrc, path.join(srcDir, ".vscode"), { recursive: true });
69
-
70
- // Rename root vscode to .vscode
71
- fs.renameSync(vscodeSrc, path.join(dir, ".vscode"));
72
- }
73
- }
74
-
75
-
76
- function patchPackageJson(dir, name) {
77
- const pkgPath = path.join(dir, "package.json");
78
- if (!fs.existsSync(pkgPath)) return;
79
-
80
- let content = fs.readFileSync(pkgPath, "utf8");
81
- content = content.replace(/__NAME__/g, name);
82
- fs.writeFileSync(pkgPath, content);
83
- }
84
-
85
-
86
- main();
package/bin/watcher.js DELETED
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import fs from "node:fs";
4
-
5
-
6
- /**
7
- * maxserver-watcher
8
- *
9
- * A zero-config minimal development server watcher.
10
- * - Watches the current project root recursively.
11
- * - Restarts the node process on .js/.json changes.
12
- * - Automatically loads .env if present via --env-file.
13
- * - Ignores node_modules and hidden files.
14
- */
15
-
16
-
17
- // Get entry file from args or default to server.js
18
- const entry = process.argv[2] || "server.js";
19
- let child;
20
-
21
- function start() {
22
- const args = [];
23
- if (fs.existsSync(".env")) args.push("--env-file=.env");
24
- args.push(entry);
25
-
26
- // Spawn the node process and share the console
27
- child = spawn("node", args, { stdio: "inherit" });
28
- }
29
-
30
- function restart() {
31
- console.clear();
32
- //console.log(`devserver restart`);
33
- if (child) child.kill();
34
- start();
35
- }
36
-
37
-
38
- fs.watch(".", { recursive: true }, (event, file) => {
39
- if (!file) return;
40
- if (file.includes("node_modules")) return;
41
- if (file.startsWith(".")) return;
42
- if (!file.endsWith(".js") && !file.endsWith(".json")) return;
43
-
44
- //console.log("\nupdated: ", file);
45
- restart();
46
- });
47
-
48
- restart();
package/index.d.ts DELETED
@@ -1,68 +0,0 @@
1
- import "fastify";
2
- import "@fastify/jwt";
3
-
4
- declare function maxserver(...args: any[]): any;
5
- export default maxserver;
6
-
7
- declare global {
8
-
9
- var global: typeof globalThis;
10
-
11
- var ENV: NodeJS.ProcessEnv & {
12
- development: boolean;
13
- production: boolean;
14
- };
15
-
16
- /** Casts a string ID to a MongoDB ObjectId using the global helper. */
17
- var oid: (id?: string) => import("mongodb").ObjectId;
18
-
19
- /** Access to the global MongoDB database instance. */
20
- var db: import("mongodb").Db;
21
-
22
- /**
23
- * Creates an Error object with an attached HTTP status code.
24
- * Fastify catches this to return a structured JSON response.
25
- * @param code The HTTP status code (e.g., 400, 401, 403, 404).
26
- * @param message The specific failure reason returned in the JSON body.
27
- */
28
- var createError: (code: number, message: string) => Error;
29
- }
30
-
31
- declare module "fastify" {
32
- interface FastifyInstance {
33
- maxserver: {
34
- env?: string;
35
- cors?: string | boolean | RegExp | Array<string | RegExp>;
36
- secret?: string;
37
- mongodb?: string;
38
- static?: string;
39
- errorLogger?: boolean;
40
- };
41
- }
42
-
43
- interface FastifyRequest {
44
- userId: string | null;
45
- }
46
-
47
- interface FastifyContextConfig {
48
- auth?: boolean;
49
- }
50
-
51
- interface RouteShorthandOptions {
52
- config?: FastifyContextConfig;
53
- }
54
- }
55
-
56
- declare module "@fastify/jwt" {
57
- interface FastifyJWT {
58
- user: {
59
- sub?: string;
60
- userId?: string;
61
- userid?: string;
62
- id?: string;
63
- [key: string]: unknown;
64
- };
65
- }
66
- }
67
-
68
- export { };
package/src/devSounds.js DELETED
@@ -1,35 +0,0 @@
1
- // Development sounds to make api development even more fun 😃
2
-
3
- import { exec } from "child_process";
4
-
5
- export async function setupDevSounds(app) {
6
- if (!app.maxserver.sounds) return;
7
- if (app.maxserver.env !== "development" || process.env.CI) return;
8
- if (process.platform !== "darwin") return;
9
-
10
- const ok = "/System/Library/Sounds/Glass.aiff";
11
- const err = "/System/Library/Sounds/Submarine.aiff";
12
-
13
- let lastPlayed = 0;
14
-
15
- const play = file => {
16
- const now = Date.now();
17
- if (now - lastPlayed < 1000) return;
18
- lastPlayed = now;
19
- exec(`afplay "${file}" >/dev/null 2>&1 &`);
20
- };
21
-
22
- app.addHook("onResponse", async (req, reply) => {
23
- const ct = String(reply.getHeader("content-type") || "").toLowerCase();
24
- const disabled = req.routeOptions?.config?.devSound === false;
25
- const trigger = ct && ct.includes("application/json") && !disabled;
26
-
27
- if (req.url === "/docs/openapi.json") return;
28
-
29
- if (trigger) {
30
- const code = reply.statusCode;
31
- if (code < 400) play(ok);
32
- else play(err);
33
- }
34
- });
35
- }
package/src/getAddress.js DELETED
@@ -1,46 +0,0 @@
1
- import os from "node:os";
2
-
3
-
4
- /**
5
- * When listening on all interfaces (0.0.0.0), Node returns "0.0.0.0".
6
- * This is a setting, not a valid URL for other devices.
7
- * So we try hunt down the actual LAN IP to print the real address on which server can be accessed
8
- */
9
-
10
-
11
- function getLanIp() {
12
- const nets = os.networkInterfaces();
13
-
14
- for (const name of Object.keys(nets)) {
15
- for (const net of (nets[name] || [])) {
16
- if (net?.family === "IPv4" && !net.internal) return net.address;
17
- }
18
- }
19
-
20
- return null;
21
- }
22
-
23
-
24
- export function getAddress(app) {
25
-
26
- const addr = app.server?.address();
27
- const protocol = app.initialConfig?.https ? "https" : "http";
28
-
29
- if (!addr) return null;
30
- if (typeof addr === "string") return addr;
31
-
32
- const isPublicBind = addr.address === "0.0.0.0" || addr.address === "::";
33
- const isLoopback = addr.address === "127.0.0.1" || addr.address === "::1";
34
-
35
- const envIp = String(process.env.PUBLIC_IP || "").trim() || null;
36
- const detectedIp = envIp || getLanIp();
37
-
38
- const ip = (isPublicBind && !isLoopback)
39
- ? (detectedIp || addr.address)
40
- : "localhost";
41
-
42
- const host = ip.includes(":") ? `[${ip}]` : ip;
43
-
44
- return `${protocol}://${host}:${addr.port}`;
45
- }
46
-
package/src/setup.js DELETED
@@ -1,104 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
-
4
- import cors from "@fastify/cors";
5
- import jwt from "@fastify/jwt";
6
- import cookie from "@fastify/cookie";
7
- import mongodb from "@fastify/mongodb";
8
- import fastifyStatic from "@fastify/static";
9
- import helmet from "@fastify/helmet";
10
-
11
- export async function setupHelmet(app) {
12
- await app.register(helmet, {
13
- contentSecurityPolicy: false,
14
- frameguard: false,
15
- crossOriginResourcePolicy: {
16
- policy: "cross-origin"
17
- }
18
- });
19
- }
20
-
21
- export async function setupCors(app) {
22
- const isProd = app.maxserver.env === "production";
23
- let origin = app.maxserver.cors ?? "*";
24
-
25
- if (origin === "*" && !isProd)
26
- origin = true;
27
-
28
- if (isProd && (origin === "*" || origin === true))
29
- app.log.warn("CORS: allowing all origins in production with credentials is risky");
30
-
31
- await app.register(cors, {
32
- origin,
33
- credentials: true,
34
- methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
35
- });
36
- }
37
-
38
- export async function setupCookie(app) {
39
- await app.register(cookie, {
40
- secret: app.maxserver.secret,
41
- hook: "onRequest"
42
- });
43
- }
44
-
45
- export async function setupMongo(app) {
46
- const url = app.maxserver.mongodb;
47
- if (!url) return;
48
- await app.register(mongodb, { url });
49
-
50
- const { ObjectId, db } = app.mongo;
51
- global.oid = id => new ObjectId(id);
52
- global.db = db;
53
- }
54
-
55
- export async function setupJwt(app) {
56
- await app.register(jwt, {
57
- secret: app.maxserver.secret,
58
- cookie: { cookieName: "token" }
59
- });
60
-
61
- app.addHook("preHandler", async function (req) {
62
- if (req.method === "OPTIONS") return;
63
-
64
- const auth = req.routeOptions?.config?.auth;
65
- if (!auth) return;
66
-
67
- await req.jwtVerify();
68
- const u = req.user;
69
- req.userId = u?.sub || u?.userId || u?.userid || u?.id || null;
70
- });
71
- }
72
-
73
- export async function setupStatic(app) {
74
- const dir = app.maxserver.static;
75
- if (!dir) return;
76
-
77
- if (typeof dir !== "string") {
78
- console.error("❌ maxserver.static must be a string path");
79
- return;
80
- }
81
-
82
- const abs = path.resolve(dir);
83
- if (!fs.existsSync(abs)) {
84
- console.error(`❌ maxserver.static not found: ${abs}`);
85
- return;
86
- }
87
-
88
- await app.register(fastifyStatic, { root: abs });
89
- }
90
-
91
- export async function setupErrorLogger(app) {
92
- if (app.maxserver.env !== "development") return;
93
-
94
- app.addHook("onError", async (req, res, error) => {
95
- console.log("\n\x1b[1;31mERROR\x1b[0m");
96
-
97
- const stackLine = error.stack.split("\n")[1];
98
- const match = stackLine.match(/([^\s()]+):(\d+):\d+/);
99
- const file = match?.[1].replace("file://" + process.cwd(), "");
100
-
101
- if (file) console.log(`${file} -> line ${match[2]}`);
102
- console.log(error.message);
103
- });
104
- }
package/src/setupDocs.js DELETED
@@ -1,78 +0,0 @@
1
- import swagger from "@fastify/swagger";
2
- import apiReference from "@scalar/fastify-api-reference";
3
-
4
-
5
- export async function setupDocs(app) {
6
-
7
- const info = app.maxserver.openapiInfo || {
8
- title: "API",
9
- version: "1.0.0",
10
- };
11
-
12
- await app.register(swagger, {
13
- openapi: {
14
- info,
15
- // OpenAPI 3.x: securitySchemes must be defined globally here not per route
16
- // Routes only add `security: [...]` that references these scheme names
17
- components: {
18
- securitySchemes: {
19
- bearerAuth: { type: "http", scheme: "bearer" },
20
- cookieAuth: { type: "apiKey", in: "cookie", name: "token" },
21
- },
22
- },
23
- },
24
- // Only consider for documentation routes which have a schema
25
- transform: ({ schema, url }) => {
26
- const isDefined = schema && (schema.summary || schema.response || schema.body);
27
- if (!isDefined) return { schema: { hide: true }, url };
28
- return { schema, url };
29
- }
30
- });
31
-
32
- // not needed, the scalar extension provides it by default on /docs/openapi.json
33
- // app.get("/openapi.json", {schema}, () => app.swagger());
34
-
35
-
36
- if (app.maxserver.docs !== false)
37
- await app.register(apiReference, {
38
- "routePrefix": "/docs",
39
- "openapi": true,
40
- "configuration": {
41
- hideSearch: true,
42
- hiddenClients: true,
43
- hideClientButton: true,
44
- telemetry: false,
45
- persistAuth: true,
46
- showDeveloperTools: "never",
47
- //"expandAllModelSections": true,
48
- // operationsSorter: "alpha",
49
- orderSchemaPropertiesBy: "preserve",
50
- metaData: {
51
- title: "Server",
52
- },
53
- authentication: {
54
- preferredSecurityScheme: 'bearerAuth',
55
- apiKey: { token: "" }
56
- },
57
- customCss: `
58
- .darklight-reference a[href*="scalar.com"] {
59
- display: none !important;
60
- },
61
- .text-sidebar-c-2 {
62
- display: none !important;
63
- }`,
64
- ...app.maxserver.scalar
65
- }
66
- });
67
-
68
- app.addHook("onRoute", (route) => {
69
- const auth = route.config?.auth;
70
- if (!auth) return;
71
- route.schema ||= {};
72
- route.schema.security ||= [{ bearerAuth: [] }, { cookieAuth: [] }];
73
- });
74
- }
75
-
76
-
77
-
78
-
@@ -1,164 +0,0 @@
1
- /**
2
- * 🚀 AUTO-LOADER
3
- * Scans src/ for files with "// METHOD /url" comments.
4
- * Automatically registers them as Fastify routes.
5
- * Also registers lonely .schema.js files as global shared schemas.
6
- */
7
-
8
- import fs from "fs";
9
- import path from "path";
10
- import { pathToFileURL } from "url";
11
-
12
- // Matches lines like: // GET /api/v1/users
13
- const ROUTE_REGEX = /^\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(.+)$/gm;
14
-
15
- /**
16
- * Recursively finds all .js files in a directory.
17
- */
18
- function walk(dir, out = []) {
19
- if (!fs.existsSync(dir)) return out;
20
-
21
- for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
22
- if (e.name === "node_modules" || e.name.startsWith(".")) continue;
23
-
24
- const full = path.join(dir, e.name);
25
- if (e.isDirectory()) {
26
- walk(full, out);
27
- continue;
28
- }
29
-
30
- if (e.name.endsWith(".js")) out.push(full);
31
- }
32
-
33
- return out;
34
- }
35
-
36
- /**
37
- * Extracts method and URL from the file's "magic comment".
38
- */
39
- function getRoute(file) {
40
- const text = fs.readFileSync(file, "utf8");
41
- const matches = [...text.matchAll(ROUTE_REGEX)];
42
-
43
- if (matches.length === 0) return null;
44
-
45
- if (matches.length > 1) {
46
- console.warn(`⚠️ Ignored "${file}": Only 1 route allowed per file.`);
47
- return null;
48
- }
49
-
50
- const m = matches[0];
51
- return {
52
- method: m[1].toLowerCase(),
53
- url: "/" + m[2].trim().replace(/^\/+/, "")
54
- };
55
- }
56
-
57
- export async function setupRoutes(app) {
58
- const root = path.resolve(app.maxserver.routesDir || "src");
59
- const files = walk(root);
60
-
61
- // 1. Pass One: Register Named Exports Globally
62
- const globals = new Map();
63
- for (const file of files) {
64
- if (file.endsWith(".schema.js")) continue;
65
-
66
- const mod = await import(pathToFileURL(file).href);
67
- for (const [key, val] of Object.entries(mod)) {
68
- if (key === "default" || key.startsWith("autoregister_")) continue;
69
-
70
- if (globals.has(key)) {
71
- console.error("\n❌ Global Identifier Conflict!");
72
- console.error(`The export "${key}" is defined in multiple files:`);
73
- console.error(` -> ${globals.get(key)}`);
74
- console.error(` -> ${file}\n`);
75
- throw new Error(`Duplicate global identifier "${key}"`);
76
- }
77
-
78
- globals.set(key, file);
79
- global[key] = val;
80
- }
81
- }
82
-
83
- // 2. Pass Two: Register Global Schemas (Lonely .schema.js files)
84
- for (const file of files) {
85
- const isLonely = file.endsWith(".schema.js") &&
86
- !fs.existsSync(file.replace(".schema.js", ".js"));
87
-
88
- if (!isLonely) continue;
89
-
90
- const mod = await import(pathToFileURL(file).href);
91
- for (const schema of Object.values(mod))
92
- if (schema?.$id) app.addSchema(schema);
93
- }
94
-
95
- // 3. Pass Three: Auto-register hooks
96
- for (const file of files) {
97
- if (file.endsWith(".schema.js")) continue;
98
-
99
- const mod = await import(pathToFileURL(file).href);
100
- for (const [key, fn] of Object.entries(mod))
101
- if (key.startsWith("autoregister_") && typeof fn === "function") await fn(app);
102
- }
103
-
104
- // 4. Pass Four: Collect and Group Routes by Directory
105
- const groups = new Map();
106
- for (const file of files) {
107
- if (file.endsWith(".schema.js")) continue;
108
-
109
- const info = getRoute(file);
110
- if (!info) continue;
111
-
112
- const mod = await import(pathToFileURL(file).href);
113
- const handler = mod.default;
114
- if (typeof handler !== "function") {
115
- throw new Error(`Route in "${file}" must export a default function.`);
116
- }
117
-
118
- const schemaFile = file.replace(/\.js$/, ".schema.js");
119
- let raw = {};
120
-
121
- if (fs.existsSync(schemaFile)) {
122
- const loaded = (await import(pathToFileURL(schemaFile).href)).default;
123
- if (loaded && typeof loaded === "object") raw = loaded;
124
- }
125
-
126
- let { auth, order = 999, routeOptions = {}, ...schema } = raw;
127
-
128
- if (auth !== undefined) {
129
- routeOptions = {
130
- ...routeOptions,
131
- config: { ...(routeOptions.config || {}), auth: !!auth },
132
- };
133
- }
134
-
135
- const dir = path.dirname(file);
136
- if (!groups.has(dir)) groups.set(dir, []);
137
-
138
- groups.get(dir).push({
139
- method: info.method,
140
- url: info.url,
141
- options: { ...routeOptions, schema },
142
- handler,
143
- order,
144
- file
145
- });
146
- }
147
-
148
- // Sort routes locally within each directory
149
- const routes = [];
150
- for (const list of groups.values()) {
151
- list.sort((a, b) => a.order - b.order);
152
- routes.push(...list);
153
- }
154
-
155
- // 5. Pass Five: Register Sorted Routes
156
- const seen = new Map();
157
- for (const r of routes) {
158
- const key = `${r.method} ${r.url}`;
159
- if (seen.has(key)) throw new Error(`Duplicate route "${key}" detected.`);
160
- seen.set(key, r.file);
161
-
162
- app[r.method](r.url, r.options, r.handler);
163
- }
164
- }
@@ -1 +0,0 @@
1
- node_modules
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "checkJs": true,
4
- "module": "ES2020",
5
- "target": "ES2020",
6
- "moduleResolution": "bundler",
7
- "strict": false,
8
- "noUnusedParameters": false,
9
- "noUnusedLocals": false,
10
- "noImplicitAny": false,
11
- "skipLibCheck": true,
12
- "types": [
13
- "node",
14
- "maxserver"
15
- ]
16
- },
17
- "include": [
18
- "src"
19
- ],
20
- "exclude": [
21
- "node_modules",
22
- ],
23
- }
@@ -1,13 +0,0 @@
1
- {
2
- "name": "__NAME__",
3
- "version": "0.1.0",
4
- "description": "",
5
- "main": "server.js",
6
- "scripts": {
7
- "start": "node server.js",
8
- "dev": "maxserver-watcher"
9
- },
10
- "type": "module",
11
- "private": "true",
12
- "dependencies": {}
13
- }
@@ -1,4 +0,0 @@
1
- @node
2
- @js
3
- @outjson
4
- ../