@stacksjs/server 0.70.87 → 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.
@@ -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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
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.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
+ }
@@ -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.87",
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.87",
56
+ "@stacksjs/config": "0.70.90",
57
57
  "better-dx": "^0.2.16",
58
- "@stacksjs/path": "0.70.87",
59
- "@stacksjs/router": "0.70.87",
60
- "@stacksjs/validation": "0.70.87",
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
  }