@stacksjs/server 0.70.88 → 0.70.90
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/dist/config-production.d.ts +22 -0
- package/dist/config-production.js +16 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +60 -0
- package/dist/controllers/base.d.ts +16 -0
- package/dist/controllers/base.js +38 -0
- package/dist/imports.d.ts +32 -0
- package/dist/imports.js +224 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/maintenance.d.ts +91 -0
- package/dist/maintenance.js +365 -0
- package/dist/proxy.d.ts +7 -0
- package/dist/proxy.js +28 -0
- package/dist/start.d.ts +1 -0
- package/dist/start.js +54 -0
- package/package.json +5 -5
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production server config - minimal, no runtime file loading
|
|
3
|
+
* This config is inlined at build time for compiled binaries
|
|
4
|
+
* @defaultValue `{ server: { host: '0.0.0.0' } }`
|
|
5
|
+
*/
|
|
6
|
+
export declare const config: {
|
|
7
|
+
app: {
|
|
8
|
+
name: unknown;
|
|
9
|
+
env: unknown;
|
|
10
|
+
debug: boolean;
|
|
11
|
+
url: unknown
|
|
12
|
+
};
|
|
13
|
+
server: {
|
|
14
|
+
port: unknown;
|
|
15
|
+
/** @defaultValue '0.0.0.0' */
|
|
16
|
+
host: string
|
|
17
|
+
};
|
|
18
|
+
logging: {
|
|
19
|
+
level: unknown
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export default config;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
app: {
|
|
3
|
+
name: process.env.APP_NAME || "Stacks",
|
|
4
|
+
env: process.env.APP_ENV || "production",
|
|
5
|
+
debug: process.env.APP_DEBUG === "true" || !1,
|
|
6
|
+
url: process.env.APP_URL || "https://stacksjs.com"
|
|
7
|
+
},
|
|
8
|
+
server: {
|
|
9
|
+
port: Number(process.env.PORT) || 3000,
|
|
10
|
+
host: "0.0.0.0"
|
|
11
|
+
},
|
|
12
|
+
logging: {
|
|
13
|
+
level: process.env.LOG_LEVEL || "info"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
export default config;
|
package/dist/config.d.ts
ADDED
package/dist/config.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ports } from "@stacksjs/config";
|
|
2
|
+
export function config(options) {
|
|
3
|
+
const serversMap = {
|
|
4
|
+
frontend: {
|
|
5
|
+
host: "localhost",
|
|
6
|
+
port: ports.frontend
|
|
7
|
+
},
|
|
8
|
+
backend: {
|
|
9
|
+
host: "localhost",
|
|
10
|
+
port: ports.backend
|
|
11
|
+
},
|
|
12
|
+
api: {
|
|
13
|
+
host: "localhost",
|
|
14
|
+
port: ports.api
|
|
15
|
+
},
|
|
16
|
+
admin: {
|
|
17
|
+
host: "localhost",
|
|
18
|
+
port: ports.admin
|
|
19
|
+
},
|
|
20
|
+
library: {
|
|
21
|
+
host: "localhost",
|
|
22
|
+
port: ports.library
|
|
23
|
+
},
|
|
24
|
+
desktop: {
|
|
25
|
+
host: "localhost",
|
|
26
|
+
port: ports.desktop
|
|
27
|
+
},
|
|
28
|
+
docs: {
|
|
29
|
+
host: "localhost",
|
|
30
|
+
port: ports.docs
|
|
31
|
+
},
|
|
32
|
+
email: {
|
|
33
|
+
host: "localhost",
|
|
34
|
+
port: ports.email
|
|
35
|
+
},
|
|
36
|
+
inspect: {
|
|
37
|
+
host: "localhost",
|
|
38
|
+
port: ports.inspect
|
|
39
|
+
},
|
|
40
|
+
"system-tray": {
|
|
41
|
+
host: "localhost",
|
|
42
|
+
port: ports.systemTray
|
|
43
|
+
},
|
|
44
|
+
database: {
|
|
45
|
+
host: "localhost",
|
|
46
|
+
port: ports.database
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
if (options.type && options.type in serversMap)
|
|
50
|
+
return {
|
|
51
|
+
host: serversMap[options.type].host,
|
|
52
|
+
port: serversMap[options.type].port,
|
|
53
|
+
open: !1
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
host: options.host || "stacks.localhost",
|
|
57
|
+
port: options.port || 3000,
|
|
58
|
+
open: !1
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ResponseData } from '@stacksjs/types';
|
|
2
|
+
declare type Request = any;
|
|
3
|
+
/**
|
|
4
|
+
* Base Controller class providing Laravel-like functionality
|
|
5
|
+
*/
|
|
6
|
+
export declare class Controller {
|
|
7
|
+
protected json(data: any, status?: number): any;
|
|
8
|
+
protected success(data: any): ResponseData;
|
|
9
|
+
protected created(data: any): ResponseData;
|
|
10
|
+
protected noContent(): any;
|
|
11
|
+
protected error(message: string, status?: number): ResponseData;
|
|
12
|
+
protected notFound(message?: string): ResponseData;
|
|
13
|
+
protected unauthorized(message?: string): ResponseData;
|
|
14
|
+
protected forbidden(message?: string): ResponseData;
|
|
15
|
+
protected validate(request: Request, rules: Record<string, any>): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { log } from "@stacksjs/logging";
|
|
2
|
+
import { response } from "@stacksjs/router";
|
|
3
|
+
|
|
4
|
+
export class Controller {
|
|
5
|
+
json(data, status = 200) {
|
|
6
|
+
return response.json(data, status);
|
|
7
|
+
}
|
|
8
|
+
success(data) {
|
|
9
|
+
return this.json(data, 200);
|
|
10
|
+
}
|
|
11
|
+
created(data) {
|
|
12
|
+
return this.json(data, 201);
|
|
13
|
+
}
|
|
14
|
+
noContent() {
|
|
15
|
+
return response.noContent();
|
|
16
|
+
}
|
|
17
|
+
error(message, status = 500) {
|
|
18
|
+
return this.json({ error: message }, status);
|
|
19
|
+
}
|
|
20
|
+
notFound(message = "Resource not found") {
|
|
21
|
+
return this.error(message, 404);
|
|
22
|
+
}
|
|
23
|
+
unauthorized(message = "Unauthorized") {
|
|
24
|
+
return this.error(message, 401);
|
|
25
|
+
}
|
|
26
|
+
forbidden(message = "Forbidden") {
|
|
27
|
+
return this.error(message, 403);
|
|
28
|
+
}
|
|
29
|
+
validate(request, rules) {
|
|
30
|
+
try {
|
|
31
|
+
const result = request.validate(rules);
|
|
32
|
+
log.info("Validation result:", result);
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return Promise.reject(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate runtime auto-import files for Bun runtime execution.
|
|
3
|
+
* This creates index files that can be imported to get all auto-imports,
|
|
4
|
+
* including defineModel()-based model definitions and resource functions.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generateAutoImportFiles(): Promise<void>;
|
|
7
|
+
/**
|
|
8
|
+
* Initialize auto-imports for both bundler and runtime.
|
|
9
|
+
*
|
|
10
|
+
* Scans defineModel() model files and resource functions, making them
|
|
11
|
+
* available globally without explicit imports.
|
|
12
|
+
*/
|
|
13
|
+
export declare function initiateImports(): void;
|
|
14
|
+
/**
|
|
15
|
+
* Import and inject all auto-imports into globalThis for runtime access.
|
|
16
|
+
* Call this early in your application startup.
|
|
17
|
+
*
|
|
18
|
+
* This makes all models, plus framework primitives used by actions
|
|
19
|
+
* (Action, response, schema, Auth), available globally, matching the
|
|
20
|
+
* "no imports needed" ergonomics of framework default actions.
|
|
21
|
+
*/
|
|
22
|
+
export declare function injectGlobalAutoImports(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Generate a runtime index file for defineModel() models.
|
|
25
|
+
* These use `export default defineModel(...)`, so we re-export each default as a named export.
|
|
26
|
+
*
|
|
27
|
+
* Each entry is either a plain string (recursive scan) or `{ dir, recursive: false }`
|
|
28
|
+
* for non-recursive scans. The defaults root passes the non-recursive form so
|
|
29
|
+
* that feature-module subdirs (commerce/, Content/, …) only ship when the
|
|
30
|
+
* project opted into them via config.
|
|
31
|
+
*/
|
|
32
|
+
declare type ScanEntry = string | { dir: string, recursive: boolean }
|
package/dist/imports.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, relative } from "node:path";
|
|
3
|
+
import { plugin } from "bun";
|
|
4
|
+
import { log } from "@stacksjs/logging";
|
|
5
|
+
import { path } from "@stacksjs/path";
|
|
6
|
+
import { autoImports, generateRuntimeIndex, generateGlobalsScript } from "bun-plugin-auto-imports";
|
|
7
|
+
import { globSync } from "@stacksjs/storage";
|
|
8
|
+
const OPTIONAL_MODEL_MODULES = {
|
|
9
|
+
commerce: ["config/commerce.ts"],
|
|
10
|
+
Content: ["config/cms.ts", "config/blog.ts"],
|
|
11
|
+
realtime: ["config/realtime.ts"]
|
|
12
|
+
};
|
|
13
|
+
function configEnabled(configRelPaths) {
|
|
14
|
+
return configRelPaths.some((rel) => existsSync(path.projectPath(rel)));
|
|
15
|
+
}
|
|
16
|
+
function resolveDefaultModelDirs() {
|
|
17
|
+
const root = path.storagePath("framework/defaults/app/Models"), dirs = [root];
|
|
18
|
+
for (const [subdir, configPaths] of Object.entries(OPTIONAL_MODEL_MODULES))
|
|
19
|
+
if (configEnabled(configPaths))
|
|
20
|
+
dirs.push(`${root}/${subdir}`);
|
|
21
|
+
return dirs;
|
|
22
|
+
}
|
|
23
|
+
function scanDirTopLevel(dir) {
|
|
24
|
+
try {
|
|
25
|
+
return globSync(`${dir}/*.ts`, { ignore: ["**/*.d.ts", "**/index.ts", "**/README*"] });
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function scanDefineModelExports(dir, opts = {}) {
|
|
31
|
+
const { recursive = !0 } = opts;
|
|
32
|
+
let files = [];
|
|
33
|
+
try {
|
|
34
|
+
const pattern = recursive ? `${dir}/**/*.ts` : `${dir}/*.ts`;
|
|
35
|
+
files = globSync(pattern, { ignore: ["**/*.d.ts", "**/index.ts", "**/README*"] });
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const exports = [], seen = new Set;
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const basename = file.split("/").pop()?.replace(".ts", "") || "";
|
|
42
|
+
if (basename && !seen.has(basename)) {
|
|
43
|
+
seen.add(basename);
|
|
44
|
+
exports.push({ name: basename, file, isDefault: !0 });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return exports;
|
|
48
|
+
}
|
|
49
|
+
const GLOBAL_SHADOW_BLOCKLIST = new Set([
|
|
50
|
+
"Error",
|
|
51
|
+
"Request",
|
|
52
|
+
"Response",
|
|
53
|
+
"URL",
|
|
54
|
+
"Map",
|
|
55
|
+
"Set",
|
|
56
|
+
"Object",
|
|
57
|
+
"Array",
|
|
58
|
+
"Number",
|
|
59
|
+
"String",
|
|
60
|
+
"Date",
|
|
61
|
+
"Promise",
|
|
62
|
+
"Symbol"
|
|
63
|
+
]);
|
|
64
|
+
async function generateDefineModelIndex(entries, outputPath) {
|
|
65
|
+
const lines = ["// Generated by bun-plugin-auto-imports"], seen = new Set;
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const dir = typeof entry === "string" ? entry : entry.dir, recursive = typeof entry === "string" ? !0 : entry.recursive;
|
|
68
|
+
let files = [];
|
|
69
|
+
try {
|
|
70
|
+
const pattern = recursive ? `${dir}/**/*.ts` : `${dir}/*.ts`;
|
|
71
|
+
files = globSync(pattern, { ignore: ["**/*.d.ts", "**/index.ts", "**/README*"] });
|
|
72
|
+
} catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const basename = file.split("/").pop()?.replace(".ts", "") || "";
|
|
77
|
+
if (!basename || seen.has(basename))
|
|
78
|
+
continue;
|
|
79
|
+
seen.add(basename);
|
|
80
|
+
const relativePath = relative(dirname(outputPath), file).replace(/\.ts$/, "");
|
|
81
|
+
if (GLOBAL_SHADOW_BLOCKLIST.has(basename)) {
|
|
82
|
+
lines.push(`// Skipped '${basename}' \u2014 would shadow a built-in global. Import directly if needed.`);
|
|
83
|
+
lines.push(`// export { default as ${basename} } from '${relativePath}'`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
lines.push(`export { default as ${basename} } from '${relativePath}'`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
await Bun.write(outputPath, lines.join(`
|
|
90
|
+
`) + `
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
export async function generateAutoImportFiles() {
|
|
94
|
+
const userFunctionsPath = path.resourcesPath("functions"), defaultFunctionsPath = path.storagePath("framework/defaults/functions"), functionsPath = userFunctionsPath, outputDir = path.storagePath("framework/auto-imports"), userModelsPath = path.userModelsPath(), defaultModelDirs = resolveDefaultModelDirs(), [defaultsRoot = path.storagePath("framework/defaults/app/Models"), ...enabledSubdirs] = defaultModelDirs, userJobsPath = path.userJobsPath(), userControllersPath = path.userControllersPath(), defaultControllersPath = path.storagePath("framework/defaults/app/Controllers");
|
|
95
|
+
await Bun.write(`${outputDir}/.gitkeep`, "");
|
|
96
|
+
const functionsIndexPath = `${outputDir}/functions.ts`;
|
|
97
|
+
await generateRuntimeIndex([userFunctionsPath, defaultFunctionsPath], functionsIndexPath);
|
|
98
|
+
const modelsIndexPath = `${outputDir}/models.ts`, modelScan = [
|
|
99
|
+
userModelsPath,
|
|
100
|
+
{ dir: defaultsRoot, recursive: !1 },
|
|
101
|
+
...defaultModelDirs.slice(1).map((d) => ({ dir: d, recursive: !0 }))
|
|
102
|
+
];
|
|
103
|
+
await generateDefineModelIndex(modelScan, modelsIndexPath);
|
|
104
|
+
const jobsIndexPath = `${outputDir}/jobs.ts`;
|
|
105
|
+
await generateDefineModelIndex([userJobsPath], jobsIndexPath);
|
|
106
|
+
const controllersIndexPath = `${outputDir}/controllers.ts`;
|
|
107
|
+
await generateDefineModelIndex([userControllersPath, defaultControllersPath], controllersIndexPath);
|
|
108
|
+
const combinedContent = `// Generated by bun-plugin-auto-imports
|
|
109
|
+
export * from './functions'
|
|
110
|
+
export * from './models'
|
|
111
|
+
export * from './jobs'
|
|
112
|
+
export * from './controllers'
|
|
113
|
+
`;
|
|
114
|
+
await Bun.write(`${outputDir}/index.ts`, combinedContent);
|
|
115
|
+
const globalsPath = `${outputDir}/globals.ts`;
|
|
116
|
+
await generateGlobalsScript([functionsPath], globalsPath, `${outputDir}/index.ts`);
|
|
117
|
+
log.debug("Auto-import files generated successfully");
|
|
118
|
+
}
|
|
119
|
+
export function initiateImports() {
|
|
120
|
+
const functionsPath = path.resourcesPath("functions"), defaultFunctionsPath = path.storagePath("framework/defaults/functions"), userModelsPath = path.userModelsPath(), defaultModelDirs = resolveDefaultModelDirs(), [defaultsRoot = path.storagePath("framework/defaults/app/Models"), ...enabledSubdirs] = defaultModelDirs, userJobsPath = path.userJobsPath(), userControllersPath = path.userControllersPath(), defaultControllersPath = path.storagePath("framework/defaults/app/Controllers"), defineModelExports = [
|
|
121
|
+
...scanDefineModelExports(userModelsPath),
|
|
122
|
+
...scanDefineModelExports(defaultsRoot, { recursive: !1 }),
|
|
123
|
+
...enabledSubdirs.flatMap((d) => scanDefineModelExports(d))
|
|
124
|
+
], jobExports = scanDefineModelExports(userJobsPath), seen = new Set, uniqueDefineModelExports = defineModelExports.filter((exp) => {
|
|
125
|
+
if (seen.has(exp.name))
|
|
126
|
+
return !1;
|
|
127
|
+
seen.add(exp.name);
|
|
128
|
+
return !0;
|
|
129
|
+
}), dtsDir = dirname(path.storagePath("framework/types/server-auto-imports.d.ts")), defineModelImports = uniqueDefineModelExports.map((exp) => ({
|
|
130
|
+
from: `./${relative(dtsDir, exp.file).replace(/\\/g, "/").replace(/\.ts$/, "")}`,
|
|
131
|
+
name: "default",
|
|
132
|
+
as: exp.name
|
|
133
|
+
})), jobImports = jobExports.map((exp) => ({
|
|
134
|
+
from: `./${relative(dtsDir, exp.file).replace(/\\/g, "/").replace(/\.ts$/, "")}`,
|
|
135
|
+
name: "default",
|
|
136
|
+
as: exp.name
|
|
137
|
+
})), controllerExports = [
|
|
138
|
+
...scanDefineModelExports(userControllersPath),
|
|
139
|
+
...scanDefineModelExports(defaultControllersPath)
|
|
140
|
+
], seenControllers = new Set, controllerImports = controllerExports.filter((exp) => {
|
|
141
|
+
if (seenControllers.has(exp.name))
|
|
142
|
+
return !1;
|
|
143
|
+
seenControllers.add(exp.name);
|
|
144
|
+
return !0;
|
|
145
|
+
}).map((exp) => ({
|
|
146
|
+
from: `./${relative(dtsDir, exp.file).replace(/\\/g, "/").replace(/\.ts$/, "")}`,
|
|
147
|
+
name: "default",
|
|
148
|
+
as: exp.name
|
|
149
|
+
})), options = {
|
|
150
|
+
dts: path.storagePath("framework/types/server-auto-imports.d.ts"),
|
|
151
|
+
imports: [...defineModelImports, ...jobImports, ...controllerImports],
|
|
152
|
+
dirs: [functionsPath, defaultFunctionsPath],
|
|
153
|
+
eslint: {
|
|
154
|
+
enabled: !0,
|
|
155
|
+
filepath: path.storagePath("framework/server-auto-imports.json")
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
plugin(autoImports(options));
|
|
159
|
+
generateAutoImportFiles().catch((err) => {
|
|
160
|
+
console.error("[Server] Failed to generate auto-import files:", err);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export async function injectGlobalAutoImports() {
|
|
164
|
+
if (globalThis.__stacksAutoImportsInjected)
|
|
165
|
+
return;
|
|
166
|
+
globalThis.__stacksAutoImportsInjected = !0;
|
|
167
|
+
const errors = [], primitiveModules = [
|
|
168
|
+
["@stacksjs/types", ["Every", "ExitCode"]],
|
|
169
|
+
["@stacksjs/path", ["path"]],
|
|
170
|
+
["@stacksjs/error-handling", ["HttpError", "handleError"]],
|
|
171
|
+
["@stacksjs/logging", ["log"]],
|
|
172
|
+
["@stacksjs/config", ["config"]],
|
|
173
|
+
["@stacksjs/validation", ["schema"]],
|
|
174
|
+
["@stacksjs/router", ["response", "request", "route", "Middleware", "url"]],
|
|
175
|
+
["@stacksjs/storage", ["storage", "fs"]],
|
|
176
|
+
["@stacksjs/orm", ["defineModel", "toAttrs"]],
|
|
177
|
+
["@stacksjs/database", ["db", "sql"]],
|
|
178
|
+
["@stacksjs/email", ["mail", "template"]],
|
|
179
|
+
["@stacksjs/queue", ["Job"]],
|
|
180
|
+
["@stacksjs/scheduler", ["schedule"]],
|
|
181
|
+
["@stacksjs/actions", ["Action"]],
|
|
182
|
+
["@stacksjs/auth", ["Auth", "register", "sessionCheck"]],
|
|
183
|
+
["@stacksjs/events", ["dispatch", "listen", "emitter"]],
|
|
184
|
+
["@stacksjs/feature-flags", ["Feature"]],
|
|
185
|
+
["@stacksjs/security", ["makeHash", "verifyHash"]],
|
|
186
|
+
["@stacksjs/collections", ["collect"]],
|
|
187
|
+
["@stacksjs/cli", ["quotes"]],
|
|
188
|
+
["@stacksjs/notifications", ["notify", "useNotification", "useEmail", "useSMS", "useChat", "useDatabase"]],
|
|
189
|
+
["@stacksjs/realtime", ["emit", "emitToUser", "emitToUsers", "createChannel", "dispatchBroadcast"]],
|
|
190
|
+
["@stacksjs/i18n", ["I18n", "t", "tc", "te", "setLocale", "getLocale"]],
|
|
191
|
+
["@stacksjs/stx", ["state", "derived", "effect"]],
|
|
192
|
+
["@stacksjs/browser", ["useDark", "usePreferredDark", "useToggle", "useStorage"]]
|
|
193
|
+
], importWithTimeout = async (pkg) => {
|
|
194
|
+
return Promise.race([
|
|
195
|
+
import(pkg),
|
|
196
|
+
new Promise((_, reject) => setTimeout(() => reject(Error(`auto-import timed out: ${pkg}`)), 4000))
|
|
197
|
+
]);
|
|
198
|
+
};
|
|
199
|
+
await Promise.all(primitiveModules.map(async ([pkg, names]) => {
|
|
200
|
+
try {
|
|
201
|
+
const mod = await importWithTimeout(pkg);
|
|
202
|
+
for (const name of names)
|
|
203
|
+
if (mod[name] !== void 0)
|
|
204
|
+
globalThis[name] = mod[name];
|
|
205
|
+
} catch (err) {
|
|
206
|
+
errors.push(err);
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
209
|
+
try {
|
|
210
|
+
const { ensureLocalesLoaded } = await import("@stacksjs/i18n");
|
|
211
|
+
await ensureLocalesLoaded();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
errors.push(err);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const autoImports = await import(path.storagePath("framework/auto-imports/index.ts"));
|
|
217
|
+
Object.assign(globalThis, autoImports);
|
|
218
|
+
} catch (err) {
|
|
219
|
+
errors.push(err);
|
|
220
|
+
}
|
|
221
|
+
if (errors.length)
|
|
222
|
+
for (const err of errors)
|
|
223
|
+
console.warn("[auto-imports]", err.message);
|
|
224
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the path to the maintenance file
|
|
3
|
+
*/
|
|
4
|
+
export declare function maintenanceFilePath(): string;
|
|
5
|
+
export declare function comingSoonFilePath(): string;
|
|
6
|
+
export declare function siteModeFilePath(mode: SiteMode): string;
|
|
7
|
+
/**
|
|
8
|
+
* Check if the application is in maintenance mode
|
|
9
|
+
*/
|
|
10
|
+
export declare function isDownForMaintenance(): Promise<boolean>;
|
|
11
|
+
export declare function isComingSoon(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Get the maintenance mode payload
|
|
14
|
+
*/
|
|
15
|
+
export declare function maintenancePayload(): Promise<MaintenancePayload | null>;
|
|
16
|
+
export declare function comingSoonPayload(): Promise<MaintenancePayload | null>;
|
|
17
|
+
export declare function siteModePayload(mode: SiteMode): Promise<MaintenancePayload | null>;
|
|
18
|
+
export declare function activeSiteModePayload(): Promise<MaintenancePayload | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Put the application into maintenance mode
|
|
21
|
+
*/
|
|
22
|
+
export declare function down(options?: Partial<MaintenancePayload>): Promise<void>;
|
|
23
|
+
export declare function comingSoon(options?: Partial<MaintenancePayload>): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Bring the application out of maintenance mode
|
|
26
|
+
*/
|
|
27
|
+
export declare function up(): Promise<void>;
|
|
28
|
+
export declare function launch(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Check if an IP address is allowed during maintenance
|
|
31
|
+
*/
|
|
32
|
+
export declare function isAllowedIp(ip: string, allowed?: string[]): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Check if a request has a valid bypass cookie
|
|
35
|
+
*/
|
|
36
|
+
export declare function bypassCookieName(mode?: SiteMode): string;
|
|
37
|
+
export declare function hasValidBypassCookie(cookies: Record<string, string>, secret: string, mode?: SiteMode): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Check if a request path matches the bypass secret
|
|
40
|
+
*/
|
|
41
|
+
export declare function isSecretPath(path: string, secret: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Generate maintenance mode HTML response
|
|
44
|
+
*/
|
|
45
|
+
export declare function maintenanceHtml(payload: MaintenancePayload): string;
|
|
46
|
+
/**
|
|
47
|
+
* Create a maintenance mode response
|
|
48
|
+
*/
|
|
49
|
+
export declare function maintenanceResponse(payload: MaintenancePayload): Response;
|
|
50
|
+
export declare function siteModeResponse(payload: MaintenancePayload): Response;
|
|
51
|
+
/**
|
|
52
|
+
* Create bypass cookie
|
|
53
|
+
*/
|
|
54
|
+
export declare function bypassCookieValue(secret: string, mode?: SiteMode): string;
|
|
55
|
+
/**
|
|
56
|
+
* Single source of truth for the maintenance / coming-soon gate.
|
|
57
|
+
*
|
|
58
|
+
* Returns a `Response` to short-circuit the request, or `null` to let
|
|
59
|
+
* normal request handling continue.
|
|
60
|
+
*
|
|
61
|
+
* Used by:
|
|
62
|
+
* - the dev server's `onRequest` hook (so the gate runs before the
|
|
63
|
+
* stx-serve view router or the API proxy ever sees the request);
|
|
64
|
+
* - the global `Maintenance` middleware in production.
|
|
65
|
+
*
|
|
66
|
+
* Order of checks (mirrors Laravel):
|
|
67
|
+
* 1. No active site-mode (no down file, no coming-soon file, no env
|
|
68
|
+
* override) → pass through.
|
|
69
|
+
* 2. Path is always-allowed (the holding page itself, email
|
|
70
|
+
* subscribe, static assets) → pass through.
|
|
71
|
+
* 3. Path matches the secret token → set mode-aware bypass cookie,
|
|
72
|
+
* redirect home.
|
|
73
|
+
* 4. Bypass cookie present OR client IP allowed → pass through.
|
|
74
|
+
* 5. Otherwise → return the active mode's response (redirect for
|
|
75
|
+
* coming-soon when a redirect URL is configured; HTML page for
|
|
76
|
+
* maintenance).
|
|
77
|
+
*/
|
|
78
|
+
export declare function maintenanceGate(req: Request): Promise<Response | null>;
|
|
79
|
+
export declare interface MaintenancePayload {
|
|
80
|
+
mode?: SiteMode
|
|
81
|
+
time: number
|
|
82
|
+
message?: string
|
|
83
|
+
title?: string
|
|
84
|
+
retry?: number
|
|
85
|
+
secret?: string
|
|
86
|
+
allowed?: string[]
|
|
87
|
+
status?: number
|
|
88
|
+
template?: string
|
|
89
|
+
redirect?: string
|
|
90
|
+
}
|
|
91
|
+
export type SiteMode = 'maintenance' | 'coming-soon';
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { log } from "@stacksjs/logging";
|
|
2
|
+
import * as p from "@stacksjs/path";
|
|
3
|
+
const DEFAULT_MAINTENANCE_PAYLOAD = {
|
|
4
|
+
mode: "maintenance",
|
|
5
|
+
status: 503,
|
|
6
|
+
message: "We are currently performing maintenance. Please check back soon."
|
|
7
|
+
}, DEFAULT_COMING_SOON_PAYLOAD = {
|
|
8
|
+
mode: "coming-soon",
|
|
9
|
+
status: 200,
|
|
10
|
+
message: "Stacks is setting up camp. Check back soon for the public launch.",
|
|
11
|
+
redirect: "/coming-soon"
|
|
12
|
+
};
|
|
13
|
+
function defaultsForMode(mode) {
|
|
14
|
+
return mode === "coming-soon" ? DEFAULT_COMING_SOON_PAYLOAD : DEFAULT_MAINTENANCE_PAYLOAD;
|
|
15
|
+
}
|
|
16
|
+
export function maintenanceFilePath() {
|
|
17
|
+
return p.storagePath("framework/down");
|
|
18
|
+
}
|
|
19
|
+
export function comingSoonFilePath() {
|
|
20
|
+
return p.storagePath("framework/coming-soon");
|
|
21
|
+
}
|
|
22
|
+
export function siteModeFilePath(mode) {
|
|
23
|
+
return mode === "coming-soon" ? comingSoonFilePath() : maintenanceFilePath();
|
|
24
|
+
}
|
|
25
|
+
export async function isDownForMaintenance() {
|
|
26
|
+
try {
|
|
27
|
+
return await Bun.file(maintenanceFilePath()).exists();
|
|
28
|
+
} catch {
|
|
29
|
+
return !1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function isComingSoon() {
|
|
33
|
+
try {
|
|
34
|
+
return await Bun.file(comingSoonFilePath()).exists();
|
|
35
|
+
} catch {
|
|
36
|
+
return !1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function maintenancePayload() {
|
|
40
|
+
return siteModePayload("maintenance");
|
|
41
|
+
}
|
|
42
|
+
export async function comingSoonPayload() {
|
|
43
|
+
return siteModePayload("coming-soon");
|
|
44
|
+
}
|
|
45
|
+
export async function siteModePayload(mode) {
|
|
46
|
+
try {
|
|
47
|
+
const file = Bun.file(siteModeFilePath(mode));
|
|
48
|
+
if (!await file.exists())
|
|
49
|
+
return null;
|
|
50
|
+
const content = await file.text();
|
|
51
|
+
return {
|
|
52
|
+
...defaultsForMode(mode),
|
|
53
|
+
...JSON.parse(content),
|
|
54
|
+
mode
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function activeSiteModePayload() {
|
|
61
|
+
return await maintenancePayload() ?? await comingSoonPayload() ?? envSiteModePayload();
|
|
62
|
+
}
|
|
63
|
+
function envSiteModePayload() {
|
|
64
|
+
if (isTruthy(process.env.APP_MAINTENANCE))
|
|
65
|
+
return {
|
|
66
|
+
...DEFAULT_MAINTENANCE_PAYLOAD,
|
|
67
|
+
mode: "maintenance",
|
|
68
|
+
time: Date.now(),
|
|
69
|
+
secret: process.env.APP_MAINTENANCE_SECRET || void 0
|
|
70
|
+
};
|
|
71
|
+
if (isTruthy(process.env.APP_COMING_SOON))
|
|
72
|
+
return {
|
|
73
|
+
...DEFAULT_COMING_SOON_PAYLOAD,
|
|
74
|
+
mode: "coming-soon",
|
|
75
|
+
time: Date.now(),
|
|
76
|
+
secret: process.env.APP_COMING_SOON_SECRET || void 0
|
|
77
|
+
};
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function isTruthy(value) {
|
|
81
|
+
return ["1", "true", "yes", "on"].includes(String(value || "").toLowerCase());
|
|
82
|
+
}
|
|
83
|
+
export async function down(options = {}) {
|
|
84
|
+
const payload = {
|
|
85
|
+
...DEFAULT_MAINTENANCE_PAYLOAD,
|
|
86
|
+
...options,
|
|
87
|
+
mode: "maintenance",
|
|
88
|
+
time: Date.now()
|
|
89
|
+
}, frameworkDir = p.storagePath("framework"), { mkdirSync, existsSync } = await import("@stacksjs/storage");
|
|
90
|
+
if (!existsSync(frameworkDir))
|
|
91
|
+
mkdirSync(frameworkDir, { recursive: !0 });
|
|
92
|
+
await Bun.write(maintenanceFilePath(), JSON.stringify(payload, null, 2));
|
|
93
|
+
log.info("Application is now in maintenance mode.");
|
|
94
|
+
if (payload.secret)
|
|
95
|
+
log.info("Maintenance bypass secret has been configured");
|
|
96
|
+
}
|
|
97
|
+
export async function comingSoon(options = {}) {
|
|
98
|
+
const payload = {
|
|
99
|
+
...DEFAULT_COMING_SOON_PAYLOAD,
|
|
100
|
+
...options,
|
|
101
|
+
mode: "coming-soon",
|
|
102
|
+
time: Date.now()
|
|
103
|
+
}, frameworkDir = p.storagePath("framework"), { mkdirSync, existsSync } = await import("@stacksjs/storage");
|
|
104
|
+
if (!existsSync(frameworkDir))
|
|
105
|
+
mkdirSync(frameworkDir, { recursive: !0 });
|
|
106
|
+
await Bun.write(comingSoonFilePath(), JSON.stringify(payload, null, 2));
|
|
107
|
+
log.info("Application is now in coming soon mode.");
|
|
108
|
+
if (payload.secret)
|
|
109
|
+
log.info("Coming soon bypass secret has been configured");
|
|
110
|
+
}
|
|
111
|
+
export async function up() {
|
|
112
|
+
const { unlinkSync, existsSync } = await import("node:fs"), filePath = maintenanceFilePath();
|
|
113
|
+
if (existsSync(filePath)) {
|
|
114
|
+
unlinkSync(filePath);
|
|
115
|
+
log.info("Application is now live.");
|
|
116
|
+
} else
|
|
117
|
+
log.info("Application is already live.");
|
|
118
|
+
}
|
|
119
|
+
export async function launch() {
|
|
120
|
+
const { unlinkSync, existsSync } = await import("node:fs"), filePath = comingSoonFilePath();
|
|
121
|
+
if (existsSync(filePath)) {
|
|
122
|
+
unlinkSync(filePath);
|
|
123
|
+
log.info("Application is out of coming soon mode.");
|
|
124
|
+
} else
|
|
125
|
+
log.info("Application is not in coming soon mode.");
|
|
126
|
+
}
|
|
127
|
+
export function isAllowedIp(ip, allowed = []) {
|
|
128
|
+
if (allowed.length === 0)
|
|
129
|
+
return !1;
|
|
130
|
+
if (["127.0.0.1", "::1", "localhost"].includes(ip))
|
|
131
|
+
return !0;
|
|
132
|
+
return allowed.includes(ip);
|
|
133
|
+
}
|
|
134
|
+
export function bypassCookieName(mode = "maintenance") {
|
|
135
|
+
return mode === "coming-soon" ? "stacks_coming_soon_bypass" : "stacks_maintenance_bypass";
|
|
136
|
+
}
|
|
137
|
+
export function hasValidBypassCookie(cookies, secret, mode = "maintenance") {
|
|
138
|
+
return cookies[bypassCookieName(mode)] === secret;
|
|
139
|
+
}
|
|
140
|
+
export function isSecretPath(path, secret) {
|
|
141
|
+
return path === `/${secret}` || path.startsWith(`/${secret}/`);
|
|
142
|
+
}
|
|
143
|
+
export function maintenanceHtml(payload) {
|
|
144
|
+
const mode = payload.mode ?? "maintenance", defaults = defaultsForMode(mode), message = escapeHtml(payload.message || defaults.message || ""), title = escapeHtml(payload.title || (mode === "coming-soon" ? "Opening Soon" : "Trail Maintenance"));
|
|
145
|
+
return `<!DOCTYPE html>
|
|
146
|
+
<html lang="en">
|
|
147
|
+
<head>
|
|
148
|
+
<meta charset="UTF-8">
|
|
149
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
150
|
+
<title>${title}</title>
|
|
151
|
+
<style>
|
|
152
|
+
@font-face {
|
|
153
|
+
font-display: swap;
|
|
154
|
+
font-family: "Campmate Script";
|
|
155
|
+
src: url("/assets/fonts/nps/CampmateScript-Regular.woff2") format("woff2");
|
|
156
|
+
}
|
|
157
|
+
@font-face {
|
|
158
|
+
font-display: swap;
|
|
159
|
+
font-family: "Switchback";
|
|
160
|
+
src: url("/assets/fonts/nps/Switchback-Regular.woff2") format("woff2");
|
|
161
|
+
}
|
|
162
|
+
@font-face {
|
|
163
|
+
font-display: swap;
|
|
164
|
+
font-family: "NPS 2026";
|
|
165
|
+
font-weight: 100 900;
|
|
166
|
+
src: url("/assets/fonts/nps/NPS_2026-variable.woff2") format("woff2");
|
|
167
|
+
}
|
|
168
|
+
* {
|
|
169
|
+
margin: 0;
|
|
170
|
+
padding: 0;
|
|
171
|
+
box-sizing: border-box;
|
|
172
|
+
}
|
|
173
|
+
body {
|
|
174
|
+
font-family: "Switchback", ui-sans-serif, system-ui, sans-serif;
|
|
175
|
+
min-height: 100vh;
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
justify-content: center;
|
|
179
|
+
background:
|
|
180
|
+
linear-gradient(180deg, rgba(10, 28, 18, 0.78), rgba(10, 28, 18, 0.94)),
|
|
181
|
+
url("/assets/images/topography.svg") center / 760px auto,
|
|
182
|
+
#0d1e16;
|
|
183
|
+
color: #fff7e1;
|
|
184
|
+
padding: 20px;
|
|
185
|
+
}
|
|
186
|
+
.container {
|
|
187
|
+
position: relative;
|
|
188
|
+
width: min(760px, 100%);
|
|
189
|
+
overflow: hidden;
|
|
190
|
+
border: 1px solid rgba(255, 240, 200, 0.28);
|
|
191
|
+
border-top: 6px solid #df9a2f;
|
|
192
|
+
border-radius: 8px;
|
|
193
|
+
padding: clamp(2rem, 7vw, 4.5rem);
|
|
194
|
+
background:
|
|
195
|
+
linear-gradient(180deg, rgba(27, 65, 40, 0.86), rgba(12, 31, 21, 0.96)),
|
|
196
|
+
#163824;
|
|
197
|
+
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.38);
|
|
198
|
+
}
|
|
199
|
+
.container::after {
|
|
200
|
+
position: absolute;
|
|
201
|
+
inset: auto 0 0;
|
|
202
|
+
height: 44%;
|
|
203
|
+
content: "";
|
|
204
|
+
background: url("/assets/images/park-ridge.svg") center bottom / cover no-repeat;
|
|
205
|
+
opacity: 0.34;
|
|
206
|
+
pointer-events: none;
|
|
207
|
+
}
|
|
208
|
+
.eyebrow {
|
|
209
|
+
position: relative;
|
|
210
|
+
z-index: 1;
|
|
211
|
+
display: flex;
|
|
212
|
+
gap: .75rem;
|
|
213
|
+
align-items: center;
|
|
214
|
+
color: #aac47d;
|
|
215
|
+
font-family: "Switchback", ui-sans-serif, system-ui, sans-serif;
|
|
216
|
+
font-size: .9rem;
|
|
217
|
+
font-weight: 800;
|
|
218
|
+
text-transform: uppercase;
|
|
219
|
+
}
|
|
220
|
+
.eyebrow::before {
|
|
221
|
+
width: 44px;
|
|
222
|
+
height: 2px;
|
|
223
|
+
content: "";
|
|
224
|
+
background: #df9a2f;
|
|
225
|
+
}
|
|
226
|
+
h1 {
|
|
227
|
+
position: relative;
|
|
228
|
+
z-index: 1;
|
|
229
|
+
margin-top: 1rem;
|
|
230
|
+
font-family: "Campmate Script", ui-serif, Georgia, serif;
|
|
231
|
+
font-size: clamp(4.5rem, 16vw, 8rem);
|
|
232
|
+
font-weight: 400;
|
|
233
|
+
line-height: .82;
|
|
234
|
+
}
|
|
235
|
+
.lead,
|
|
236
|
+
.message,
|
|
237
|
+
.retry {
|
|
238
|
+
position: relative;
|
|
239
|
+
z-index: 1;
|
|
240
|
+
max-width: 560px;
|
|
241
|
+
color: rgba(255, 247, 225, .84);
|
|
242
|
+
font-size: 1.08rem;
|
|
243
|
+
line-height: 1.65;
|
|
244
|
+
}
|
|
245
|
+
.lead {
|
|
246
|
+
margin-top: 1.25rem;
|
|
247
|
+
color: #b8d9cf;
|
|
248
|
+
font-family: "NPS 2026", "Switchback", ui-sans-serif, system-ui, sans-serif;
|
|
249
|
+
font-size: 1.22rem;
|
|
250
|
+
font-weight: 850;
|
|
251
|
+
line-height: 1.3;
|
|
252
|
+
text-transform: uppercase;
|
|
253
|
+
}
|
|
254
|
+
.message {
|
|
255
|
+
margin-top: .75rem;
|
|
256
|
+
}
|
|
257
|
+
.retry {
|
|
258
|
+
margin-top: 1.4rem;
|
|
259
|
+
color: #aac47d;
|
|
260
|
+
font-family: "Switchback", ui-sans-serif, system-ui, sans-serif;
|
|
261
|
+
font-size: .95rem;
|
|
262
|
+
font-weight: 800;
|
|
263
|
+
text-transform: uppercase;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
266
|
+
</head>
|
|
267
|
+
<body>
|
|
268
|
+
<div class="container">
|
|
269
|
+
<div class="eyebrow">${mode === "coming-soon" ? "Stacks basecamp" : "Service notice"}</div>
|
|
270
|
+
<h1>${title}</h1>
|
|
271
|
+
<p class="lead">${mode === "coming-soon" ? "The public trailhead is almost ready." : "The route is temporarily closed while the crew improves the path."}</p>
|
|
272
|
+
<p class="message">${message}</p>
|
|
273
|
+
${payload.retry ? `<p class="retry">Estimated reopening: ${Math.ceil(payload.retry / 60)} minutes.</p>` : ""}
|
|
274
|
+
</div>
|
|
275
|
+
</body>
|
|
276
|
+
</html>`;
|
|
277
|
+
}
|
|
278
|
+
function escapeHtml(value) {
|
|
279
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
280
|
+
}
|
|
281
|
+
export function maintenanceResponse(payload) {
|
|
282
|
+
return siteModeResponse(payload);
|
|
283
|
+
}
|
|
284
|
+
export function siteModeResponse(payload) {
|
|
285
|
+
const headers = {
|
|
286
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
287
|
+
};
|
|
288
|
+
if (payload.retry)
|
|
289
|
+
headers["Retry-After"] = String(payload.retry);
|
|
290
|
+
if (payload.redirect)
|
|
291
|
+
return new Response(null, {
|
|
292
|
+
status: 302,
|
|
293
|
+
headers: { Location: payload.redirect }
|
|
294
|
+
});
|
|
295
|
+
return new Response(maintenanceHtml(payload), {
|
|
296
|
+
status: payload.status || (payload.mode === "coming-soon" ? 200 : 503),
|
|
297
|
+
headers
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
export function bypassCookieValue(secret, mode = "maintenance") {
|
|
301
|
+
return `${bypassCookieName(mode)}=${secret}; Path=/; HttpOnly; SameSite=Lax`;
|
|
302
|
+
}
|
|
303
|
+
const ALWAYS_ALLOWED_PATHS = new Set([
|
|
304
|
+
"/coming-soon",
|
|
305
|
+
"/api/email/subscribe",
|
|
306
|
+
"/favicon.ico"
|
|
307
|
+
]), ALWAYS_ALLOWED_PREFIXES = [
|
|
308
|
+
"/css/",
|
|
309
|
+
"/js/",
|
|
310
|
+
"/images/",
|
|
311
|
+
"/fonts/",
|
|
312
|
+
"/assets/",
|
|
313
|
+
"/_modules/",
|
|
314
|
+
"/@vite/",
|
|
315
|
+
"/@fs/",
|
|
316
|
+
"/__deps/"
|
|
317
|
+
];
|
|
318
|
+
function isAlwaysAllowed(path) {
|
|
319
|
+
if (ALWAYS_ALLOWED_PATHS.has(path))
|
|
320
|
+
return !0;
|
|
321
|
+
return ALWAYS_ALLOWED_PREFIXES.some((p) => path.startsWith(p));
|
|
322
|
+
}
|
|
323
|
+
function parseCookieHeader(header) {
|
|
324
|
+
const out = {};
|
|
325
|
+
if (!header)
|
|
326
|
+
return out;
|
|
327
|
+
for (const part of header.split(";")) {
|
|
328
|
+
const trimmed = part.trim(), eq = trimmed.indexOf("=");
|
|
329
|
+
if (eq === -1)
|
|
330
|
+
continue;
|
|
331
|
+
const k = trimmed.slice(0, eq).trim(), v = trimmed.slice(eq + 1).trim();
|
|
332
|
+
if (k)
|
|
333
|
+
out[k] = v;
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function clientIp(req) {
|
|
338
|
+
const fwd = req.headers.get("x-forwarded-for");
|
|
339
|
+
if (fwd)
|
|
340
|
+
return fwd.split(",")[0]?.trim() ?? "127.0.0.1";
|
|
341
|
+
const real = req.headers.get("x-real-ip");
|
|
342
|
+
if (real)
|
|
343
|
+
return real;
|
|
344
|
+
return "127.0.0.1";
|
|
345
|
+
}
|
|
346
|
+
export async function maintenanceGate(req) {
|
|
347
|
+
const payload = await activeSiteModePayload();
|
|
348
|
+
if (!payload)
|
|
349
|
+
return null;
|
|
350
|
+
const mode = payload.mode ?? "maintenance", path = new URL(req.url).pathname;
|
|
351
|
+
if (isAlwaysAllowed(path))
|
|
352
|
+
return null;
|
|
353
|
+
if (payload.secret && isSecretPath(path, payload.secret))
|
|
354
|
+
return new Response(null, {
|
|
355
|
+
status: 302,
|
|
356
|
+
headers: {
|
|
357
|
+
Location: "/",
|
|
358
|
+
"Set-Cookie": bypassCookieValue(payload.secret, mode)
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
const cookies = parseCookieHeader(req.headers.get("cookie")), hasCookie = !!payload.secret && hasValidBypassCookie(cookies, payload.secret, mode), ipAllowed = isAllowedIp(clientIp(req), payload.allowed);
|
|
362
|
+
if (hasCookie || ipAllowed)
|
|
363
|
+
return null;
|
|
364
|
+
return siteModeResponse(payload);
|
|
365
|
+
}
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**` prefix, or uses a verb that never matches a
|
|
2
|
+
* static stx page render. Without the verb rule,
|
|
3
|
+
* `route.post('/subscribe', ...)` declared at the root hits stx-serve
|
|
4
|
+
* and 404s.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isApiBoundRequest(req: Request, pathname: string): boolean;
|
|
7
|
+
export declare function proxyToBackend(req: Request, backendBase: string, stripPrefix?: string): Promise<Response>;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const API_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
2
|
+
export function isApiBoundRequest(req, pathname) {
|
|
3
|
+
return pathname.startsWith("/api/") || API_METHODS.has(req.method);
|
|
4
|
+
}
|
|
5
|
+
export async function proxyToBackend(req, backendBase, stripPrefix) {
|
|
6
|
+
const incoming = new URL(req.url);
|
|
7
|
+
let pathname = incoming.pathname;
|
|
8
|
+
if (stripPrefix && (pathname === stripPrefix || pathname.startsWith(`${stripPrefix}/`)))
|
|
9
|
+
pathname = pathname.slice(stripPrefix.length) || "/";
|
|
10
|
+
const target = `${backendBase}${pathname}${incoming.search}`, fwd = new Headers(req.headers);
|
|
11
|
+
fwd.delete("host");
|
|
12
|
+
fwd.delete("content-length");
|
|
13
|
+
fwd.set("x-forwarded-host", incoming.host);
|
|
14
|
+
fwd.set("x-forwarded-proto", incoming.protocol.replace(":", ""));
|
|
15
|
+
const body = req.method === "GET" || req.method === "HEAD" ? void 0 : await req.arrayBuffer(), upstream = await fetch(target, {
|
|
16
|
+
method: req.method,
|
|
17
|
+
headers: fwd,
|
|
18
|
+
body,
|
|
19
|
+
redirect: "manual"
|
|
20
|
+
}), out = new Headers(upstream.headers);
|
|
21
|
+
out.delete("content-length");
|
|
22
|
+
out.delete("content-encoding");
|
|
23
|
+
return new Response(upstream.body, {
|
|
24
|
+
status: upstream.status,
|
|
25
|
+
statusText: upstream.statusText,
|
|
26
|
+
headers: out
|
|
27
|
+
});
|
|
28
|
+
}
|
package/dist/start.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/start.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
globalThis.__STACKS_BINARY_MODE__ = !0;
|
|
2
|
+
import { assertRouteMiddlewareResolvable, loadRoutes, serve } from "@stacksjs/router";
|
|
3
|
+
import { log, report } from "@stacksjs/logging";
|
|
4
|
+
import config from "./config-production";
|
|
5
|
+
import routeRegistry from "../../../../../app/Routes";
|
|
6
|
+
process.on("unhandledRejection", (reason) => {
|
|
7
|
+
report(reason, { label: "[server] unhandledRejection" });
|
|
8
|
+
});
|
|
9
|
+
process.on("uncaughtException", (error) => {
|
|
10
|
+
report(error, { label: "[server] uncaughtException" });
|
|
11
|
+
log.flush().finally(() => process.exit(1));
|
|
12
|
+
});
|
|
13
|
+
console.log("[START] Application starting...");
|
|
14
|
+
console.log("[START] Node version:", process.version);
|
|
15
|
+
console.log("[START] Working directory:", process.cwd());
|
|
16
|
+
console.log("[START] Environment:", process.env.APP_ENV || "not set");
|
|
17
|
+
process.env.SKIP_CONFIG_LOADING = "true";
|
|
18
|
+
console.log("[START] Config loaded:", {
|
|
19
|
+
port: config.server.port,
|
|
20
|
+
host: config.server.host,
|
|
21
|
+
appName: config.app.name,
|
|
22
|
+
appUrl: config.app.url
|
|
23
|
+
});
|
|
24
|
+
console.log("[START] Loading routes from registry...");
|
|
25
|
+
loadRoutes(routeRegistry).then(async () => {
|
|
26
|
+
console.log("[START] Routes loaded successfully");
|
|
27
|
+
try {
|
|
28
|
+
await import("../../orm/routes");
|
|
29
|
+
console.log("[START] ORM routes loaded successfully");
|
|
30
|
+
} catch (ormError) {
|
|
31
|
+
console.warn("[START] ORM routes skipped:", ormError instanceof Error ? ormError.message : String(ormError));
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
await assertRouteMiddlewareResolvable();
|
|
35
|
+
console.log("[START] Route middleware validated");
|
|
36
|
+
} catch (middlewareError) {
|
|
37
|
+
console.error("[START] FATAL: unresolvable route middleware \u2014 refusing to serve unprotected routes:", middlewareError instanceof Error ? middlewareError.message : String(middlewareError));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
console.log("[START] Calling serve()...");
|
|
41
|
+
try {
|
|
42
|
+
serve({
|
|
43
|
+
port: config.server.port,
|
|
44
|
+
host: config.server.host
|
|
45
|
+
});
|
|
46
|
+
console.log("[START] serve() called successfully");
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("[START] ERROR calling serve():", error);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}).catch((error) => {
|
|
52
|
+
console.error("[START] ERROR loading routes:", error);
|
|
53
|
+
process.exit(1);
|
|
54
|
+
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/server",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.90",
|
|
6
6
|
"description": "Local development and production-ready.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -53,11 +53,11 @@
|
|
|
53
53
|
"prepublishOnly": "bun run build"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@stacksjs/config": "0.70.
|
|
56
|
+
"@stacksjs/config": "0.70.90",
|
|
57
57
|
"better-dx": "^0.2.16",
|
|
58
|
-
"@stacksjs/path": "0.70.
|
|
59
|
-
"@stacksjs/router": "0.70.
|
|
60
|
-
"@stacksjs/validation": "0.70.
|
|
58
|
+
"@stacksjs/path": "0.70.90",
|
|
59
|
+
"@stacksjs/router": "0.70.90",
|
|
60
|
+
"@stacksjs/validation": "0.70.90",
|
|
61
61
|
"bun-plugin-auto-imports": "^0.4.0"
|
|
62
62
|
}
|
|
63
63
|
}
|