appstage 0.2.16 → 0.2.18

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/index.cjs CHANGED
@@ -48,7 +48,7 @@ const maxLanguages = 3;
48
48
  async function resolve(...parts) {
49
49
  let fullPath = (0, node_path.join)(...parts);
50
50
  try {
51
- if ((await (0, node_fs_promises.lstat)(fullPath)).isFile()) return fullPath;
51
+ if ((await (0, node_fs_promises.lstat)(fullPath)).isFile()) return (0, node_path.resolve)(fullPath);
52
52
  } catch {}
53
53
  return null;
54
54
  }
@@ -88,23 +88,30 @@ const files = (params) => {
88
88
  let p = typeof params === "string" ? { base: params } : params;
89
89
  let bases = Array.isArray(p.base) ? p.base : [p.base];
90
90
  let exts = p.extensions ?? defaultExtensions;
91
- return async (req, res) => {
91
+ let fallthrough = p.fallthrough ?? true;
92
+ return async (req, res, next) => {
92
93
  let langs = (p.languages ?? defaultLanguages)(req);
93
94
  let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
94
95
  if (!matches(path, p.matches)) {
95
- emitLog(req.app, "Unmatched path", { data: { path } });
96
- res.status(404).send(await req.app.renderStatus?.(req, res, {
97
- code: "unmatched_path",
98
- path
99
- }));
96
+ if (fallthrough) next();
97
+ else {
98
+ emitLog(req.app, "Unmatched path", { data: { path } });
99
+ res.status(404).send(await req.app.renderStatus?.(req, res, {
100
+ code: "unmatched_path",
101
+ path
102
+ }));
103
+ }
100
104
  return;
101
105
  }
102
106
  if (path.includes("../")) {
103
- emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
104
- res.status(400).send(await req.app.renderStatus?.(req, res, {
105
- code: "invalid_path",
106
- path
107
- }));
107
+ if (fallthrough) next();
108
+ else {
109
+ emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
110
+ res.status(400).send(await req.app.renderStatus?.(req, res, {
111
+ code: "invalid_path",
112
+ path
113
+ }));
114
+ }
108
115
  return;
109
116
  }
110
117
  let filePath = null;
@@ -121,11 +128,14 @@ const files = (params) => {
121
128
  for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
122
129
  }
123
130
  if (filePath === null) {
124
- emitLog(req.app, "Unknown path", { data: { path } });
125
- res.status(404).send(await req.app.renderStatus?.(req, res, {
126
- code: "unknown_path",
127
- path
128
- }));
131
+ if (fallthrough) next();
132
+ else {
133
+ emitLog(req.app, "Unknown path", { data: { path } });
134
+ res.status(404).send(await req.app.renderStatus?.(req, res, {
135
+ code: "unknown_path",
136
+ path
137
+ }));
138
+ }
129
139
  return;
130
140
  }
131
141
  if (!p.transform?.length) {
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ type FilesParams = {
24
24
  extensions?: string[];
25
25
  languages?: (req: Request) => string[];
26
26
  transform?: TransformContent[];
27
+ fallthrough?: boolean;
27
28
  };
28
29
  /**
29
30
  * Serves files from the specified directory path in a locale-aware
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { access, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
- import { basename, extname, join, posix, relative, sep } from "node:path";
2
+ import { basename, extname, join, posix, relative, resolve, sep } from "node:path";
3
3
  import { formatDate, formatDuration } from "dateshape";
4
4
  import { randomBytes } from "node:crypto";
5
5
  import { STATUS_CODES } from "node:http";
@@ -19,10 +19,10 @@ function emitLog(app, message, payload) {
19
19
  }
20
20
 
21
21
  const maxLanguages = 3;
22
- async function resolve(...parts) {
22
+ async function resolve$1(...parts) {
23
23
  let fullPath = join(...parts);
24
24
  try {
25
- if ((await lstat(fullPath)).isFile()) return fullPath;
25
+ if ((await lstat(fullPath)).isFile()) return resolve(fullPath);
26
26
  } catch {}
27
27
  return null;
28
28
  }
@@ -62,44 +62,54 @@ const files = (params) => {
62
62
  let p = typeof params === "string" ? { base: params } : params;
63
63
  let bases = Array.isArray(p.base) ? p.base : [p.base];
64
64
  let exts = p.extensions ?? defaultExtensions;
65
- return async (req, res) => {
65
+ let fallthrough = p.fallthrough ?? true;
66
+ return async (req, res, next) => {
66
67
  let langs = (p.languages ?? defaultLanguages)(req);
67
68
  let path = typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
68
69
  if (!matches(path, p.matches)) {
69
- emitLog(req.app, "Unmatched path", { data: { path } });
70
- res.status(404).send(await req.app.renderStatus?.(req, res, {
71
- code: "unmatched_path",
72
- path
73
- }));
70
+ if (fallthrough) next();
71
+ else {
72
+ emitLog(req.app, "Unmatched path", { data: { path } });
73
+ res.status(404).send(await req.app.renderStatus?.(req, res, {
74
+ code: "unmatched_path",
75
+ path
76
+ }));
77
+ }
74
78
  return;
75
79
  }
76
80
  if (path.includes("../")) {
77
- emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
78
- res.status(400).send(await req.app.renderStatus?.(req, res, {
79
- code: "invalid_path",
80
- path
81
- }));
81
+ if (fallthrough) next();
82
+ else {
83
+ emitLog(req.app, "Invalid path (potential traversal attempt)", { data: { path } });
84
+ res.status(400).send(await req.app.renderStatus?.(req, res, {
85
+ code: "invalid_path",
86
+ path
87
+ }));
88
+ }
82
89
  return;
83
90
  }
84
91
  let filePath = null;
85
92
  for (let k = 0; k < bases.length && filePath === null; k++) {
86
93
  let base = bases[k];
87
94
  if (!path.endsWith("/")) {
88
- for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve(base, `${path}.${langs[i]}`);
89
- if (filePath === null) filePath = await resolve(base, path);
90
- for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}.${exts[j]}`);
91
- for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, `${path}.${exts[i]}`);
95
+ for (let i = 0; i < langs.length && filePath === null; i++) filePath = await resolve$1(base, `${path}.${langs[i]}`);
96
+ if (filePath === null) filePath = await resolve$1(base, path);
97
+ for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve$1(base, `${path}.${langs[i]}.${exts[j]}`);
98
+ for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve$1(base, `${path}.${exts[i]}`);
92
99
  }
93
- for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, `${path}.${langs[i]}`, `index.${exts[j]}`);
94
- for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve(base, path, `index.${langs[i]}.${exts[j]}`);
95
- for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve(base, path, `index.${exts[i]}`);
100
+ for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve$1(base, `${path}.${langs[i]}`, `index.${exts[j]}`);
101
+ for (let i = 0; i < langs.length && filePath === null; i++) for (let j = 0; j < exts.length && filePath === null; j++) filePath = await resolve$1(base, path, `index.${langs[i]}.${exts[j]}`);
102
+ for (let i = 0; i < exts.length && filePath === null; i++) filePath = await resolve$1(base, path, `index.${exts[i]}`);
96
103
  }
97
104
  if (filePath === null) {
98
- emitLog(req.app, "Unknown path", { data: { path } });
99
- res.status(404).send(await req.app.renderStatus?.(req, res, {
100
- code: "unknown_path",
101
- path
102
- }));
105
+ if (fallthrough) next();
106
+ else {
107
+ emitLog(req.app, "Unknown path", { data: { path } });
108
+ res.status(404).send(await req.app.renderStatus?.(req, res, {
109
+ code: "unknown_path",
110
+ path
111
+ }));
112
+ }
103
113
  return;
104
114
  }
105
115
  if (!p.transform?.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstage",
3
- "version": "0.2.16",
3
+ "version": "0.2.18",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -1,5 +1,5 @@
1
1
  import { lstat, readFile } from "node:fs/promises";
2
- import { basename, extname, join } from "node:path";
2
+ import { basename, extname, join, resolve as resolveAbsPath } from "node:path";
3
3
  import type { Request } from "express";
4
4
  import type { Controller } from "../types/Controller.ts";
5
5
  import type { TransformContent } from "../types/TransformContent.ts";
@@ -17,7 +17,7 @@ const maxLanguages = 3;
17
17
  async function resolve(...parts: string[]) {
18
18
  let fullPath = join(...parts);
19
19
  try {
20
- if ((await lstat(fullPath)).isFile()) return fullPath;
20
+ if ((await lstat(fullPath)).isFile()) return resolveAbsPath(fullPath);
21
21
  } catch {}
22
22
  return null;
23
23
  }
@@ -69,6 +69,7 @@ export type FilesParams = {
69
69
  extensions?: string[];
70
70
  languages?: (req: Request) => string[];
71
71
  transform?: TransformContent[];
72
+ fallthrough?: boolean;
72
73
  };
73
74
 
74
75
  const defaultExtensions = ["html", "htm"];
@@ -84,37 +85,44 @@ export const files: Controller<string | FilesParams> = (params) => {
84
85
 
85
86
  let bases = Array.isArray(p.base) ? p.base : [p.base];
86
87
  let exts = p.extensions ?? defaultExtensions;
88
+ let fallthrough = p.fallthrough ?? true;
87
89
 
88
- return async (req, res) => {
90
+ return async (req, res, next) => {
89
91
  let langs = (p.languages ?? defaultLanguages)(req);
90
92
 
91
93
  let path =
92
94
  typeof p.path === "string" ? p.path : (p.path ?? defaultPath)(req);
93
95
 
94
96
  if (!matches(path, p.matches)) {
95
- emitLog(req.app, "Unmatched path", { data: { path } });
96
-
97
- res.status(404).send(
98
- await req.app.renderStatus?.(req, res, {
99
- code: "unmatched_path",
100
- path,
101
- }),
102
- );
97
+ if (fallthrough) next();
98
+ else {
99
+ emitLog(req.app, "Unmatched path", { data: { path } });
100
+
101
+ res.status(404).send(
102
+ await req.app.renderStatus?.(req, res, {
103
+ code: "unmatched_path",
104
+ path,
105
+ }),
106
+ );
107
+ }
103
108
 
104
109
  return;
105
110
  }
106
111
 
107
112
  if (path.includes("../")) {
108
- emitLog(req.app, "Invalid path (potential traversal attempt)", {
109
- data: { path },
110
- });
111
-
112
- res.status(400).send(
113
- await req.app.renderStatus?.(req, res, {
114
- code: "invalid_path",
115
- path,
116
- }),
117
- );
113
+ if (fallthrough) next();
114
+ else {
115
+ emitLog(req.app, "Invalid path (potential traversal attempt)", {
116
+ data: { path },
117
+ });
118
+
119
+ res.status(400).send(
120
+ await req.app.renderStatus?.(req, res, {
121
+ code: "invalid_path",
122
+ path,
123
+ }),
124
+ );
125
+ }
118
126
 
119
127
  return;
120
128
  }
@@ -167,14 +175,17 @@ export const files: Controller<string | FilesParams> = (params) => {
167
175
  }
168
176
 
169
177
  if (filePath === null) {
170
- emitLog(req.app, "Unknown path", { data: { path } });
171
-
172
- res.status(404).send(
173
- await req.app.renderStatus?.(req, res, {
174
- code: "unknown_path",
175
- path,
176
- }),
177
- );
178
+ if (fallthrough) next();
179
+ else {
180
+ emitLog(req.app, "Unknown path", { data: { path } });
181
+
182
+ res.status(404).send(
183
+ await req.app.renderStatus?.(req, res, {
184
+ code: "unknown_path",
185
+ path,
186
+ }),
187
+ );
188
+ }
178
189
 
179
190
  return;
180
191
  }