mikser-io 11.6.0 → 11.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "11.6.0",
3
+ "version": "11.7.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/routes.js CHANGED
@@ -50,7 +50,12 @@ const DEFAULT_AUTH_LABEL = {
50
50
  // an origin is known — public --url / config.url wins (clickable,
51
51
  // shareable), localhost:port is the dev fallback — bare path when the
52
52
  // engine doesn't own a listener (external-app embedding, no url set).
53
- export function routeLocation(displayPath) {
53
+ export function routeLocation(displayPath, host) {
54
+ // A host-scoped route HAS an origin, and it is not this server's. Printing
55
+ // `http://localhost:3001//drive.example.com/` names a place nothing
56
+ // answers. Protocol-relative because the scheme belongs to whatever
57
+ // terminates TLS in front, which this process does not know.
58
+ if (host) return `//${host}${displayPath === '/' ? '/' : displayPath}`
54
59
  const origin = runtime.options.url
55
60
  ?? (runtime.options.port ? `http://localhost:${runtime.options.port}` : null)
56
61
  return origin ? `${origin}${displayPath}` : displayPath
@@ -62,12 +67,21 @@ export function routeLocation(displayPath) {
62
67
  // '/drive') answers for its own requests rather than its parent's. Exported
63
68
  // because the CORS middleware needs the same answer Express will reach, and a
64
69
  // second implementation of "which mount is this" would drift from this one.
65
- export function routeFor(requestPath) {
70
+ export function routeFor(requestPath, host) {
66
71
  if (!requestPath) return null
67
72
  let best = null
68
73
  for (const route of runtime.routes ?? []) {
74
+ // A host-scoped route answers for its own domain and no other.
75
+ if (route.host && route.host !== host) continue
69
76
  const base = route.path
70
- if (requestPath !== base && !requestPath.startsWith(`${base}/`)) continue
77
+ // A host-scoped route mounted at `/` owns every path on that host —
78
+ // it IS the server there. Prefix matching cannot express that, since
79
+ // nothing starts with `//`. An unscoped route at `/` is left alone:
80
+ // it would otherwise swallow the whole site.
81
+ const owns = base === '/' && route.host
82
+ ? true
83
+ : requestPath === base || requestPath.startsWith(`${base}/`)
84
+ if (!owns) continue
71
85
  if (!best || base.length > best.path.length) best = route
72
86
  }
73
87
  return best
@@ -87,6 +101,10 @@ export function routeFor(requestPath) {
87
101
  // facade must disable buffering for it (Caddy
88
102
  // flush_interval -1, nginx proxy_buffering off).
89
103
  // Default false.
104
+ // host when set, the mount answers only for this Host. Used
105
+ // by a route that takes a domain of its own — a WebDAV
106
+ // share at `/` on `drive.example.com`, say, which would
107
+ // otherwise shadow every page on the site.
90
108
  // cors `false` to emit no CORS headers for this mount. The
91
109
  // same word as the global `--no-cors` /
92
110
  // `config.server.cors: false`, meaning the same thing,
@@ -138,6 +156,7 @@ export function registerRoute({
138
156
  streaming = false,
139
157
  methods = null,
140
158
  cors,
159
+ host,
141
160
  label,
142
161
  detail,
143
162
  displayPath,
@@ -162,6 +181,7 @@ export function registerRoute({
162
181
  const descriptor = { path, plugin, reachability, streaming }
163
182
  if (methods) descriptor.methods = methods.map(m => m.toUpperCase())
164
183
  if (cors === false) descriptor.cors = false
184
+ if (host) descriptor.host = host
165
185
 
166
186
  // Dedup by path — a re-register (same path) replaces rather than
167
187
  // duplicates. Mounts happen once per process, but this keeps the
@@ -172,7 +192,7 @@ export function registerRoute({
172
192
 
173
193
  const logger = useLogger()
174
194
  if (logger) {
175
- const location = routeLocation(displayPath ?? path)
195
+ const location = routeLocation(displayPath ?? path, host)
176
196
  const bracket = authLabel ?? DEFAULT_AUTH_LABEL[reachability]
177
197
  logger.info('%s mounted: %s [%s]%s',
178
198
  label ?? plugin, location, bracket, detail ? ` ${detail}` : '')
package/src/server.js CHANGED
@@ -223,7 +223,7 @@ export function setupServer() {
223
223
  // because routes are registered at onLoaded and this middleware
224
224
  // at onLoad — it is mounted before any of them exist, and every
225
225
  // request arrives long after they all do.
226
- const route = routeFor(req.path ?? req.url)
226
+ const route = routeFor(req.path ?? req.url, req.hostname)
227
227
 
228
228
  // Does that mount answer OPTIONS itself?
229
229
  //
@@ -249,6 +249,36 @@ export function setupServer() {
249
249
  // PROPFIND, LOCK and the rest.
250
250
  const ownsPreflight = Boolean(route?.methods?.includes('OPTIONS'))
251
251
 
252
+ // A bare OPTIONS is not a preflight, and must not be answered as
253
+ // one.
254
+ //
255
+ // A CORS preflight is defined by its headers: the browser sends
256
+ // `Access-Control-Request-Method`, and without it the request is
257
+ // something else entirely. This middleware used to answer ALL of
258
+ // them — so every path on the site, routed or not, replied 204 to
259
+ // any OPTIONS with a list of five REST verbs and no `DAV:` header.
260
+ //
261
+ // That is a manufactured "I exist and I am not WebDAV", and it is
262
+ // a stronger negative than the 404 a static path would otherwise
263
+ // give. The Microsoft WebDAV redirector establishes a session by
264
+ // walking up from the path it was given, and on a mikser site
265
+ // every level of that walk answered no — including `/` and
266
+ // `/drive`, neither of which is anything. Deployments that DO map
267
+ // on Windows (Nextcloud at /remote.php/dav, SharePoint) sit behind
268
+ // hosts that simply have nothing to say at those paths.
269
+ //
270
+ // Falling through costs nothing a browser wanted: a real preflight
271
+ // still carries the header and is still answered below, and a
272
+ // non-preflight OPTIONS now reaches whatever owns the path — the
273
+ // mount's own discovery response, or the static handler's 404.
274
+ //
275
+ // `origin: false` is how the package steps aside: with a falsy
276
+ // origin it never builds an originCallback, so it calls next()
277
+ // without touching the response (cors/lib/index.js:210-228).
278
+ if (req.method === 'OPTIONS' && !req.headers['access-control-request-method']) {
279
+ return callback(null, { origin: false })
280
+ }
281
+
252
282
  // A route that wants no CORS headers at all.
253
283
  //
254
284
  // `origin: false` is the cors package's way of emitting none — the