mikser-io 11.4.0 → 11.6.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.4.0",
3
+ "version": "11.6.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/routes.js CHANGED
@@ -56,6 +56,23 @@ export function routeLocation(displayPath) {
56
56
  return origin ? `${origin}${displayPath}` : displayPath
57
57
  }
58
58
 
59
+ // The registered route a request path falls inside, or null.
60
+ //
61
+ // Longest prefix wins, so a mount nested under another ('/drive/notes' under
62
+ // '/drive') answers for its own requests rather than its parent's. Exported
63
+ // because the CORS middleware needs the same answer Express will reach, and a
64
+ // second implementation of "which mount is this" would drift from this one.
65
+ export function routeFor(requestPath) {
66
+ if (!requestPath) return null
67
+ let best = null
68
+ for (const route of runtime.routes ?? []) {
69
+ const base = route.path
70
+ if (requestPath !== base && !requestPath.startsWith(`${base}/`)) continue
71
+ if (!best || base.length > best.path.length) best = route
72
+ }
73
+ return best
74
+ }
75
+
59
76
  // Declare a mounted route. Records the proxy-relevant descriptor on
60
77
  // runtime.routes AND emits the standard mount log.
61
78
  //
@@ -70,6 +87,26 @@ export function routeLocation(displayPath) {
70
87
  // facade must disable buffering for it (Caddy
71
88
  // flush_interval -1, nginx proxy_buffering off).
72
89
  // Default false.
90
+ // cors `false` to emit no CORS headers for this mount. The
91
+ // same word as the global `--no-cors` /
92
+ // `config.server.cors: false`, meaning the same thing,
93
+ // scoped to one route. Default undefined — inherit the
94
+ // global setting.
95
+ //
96
+ // For a mount whose clients are not browsers: an OS
97
+ // WebDAV redirector, a CLI tool and curl never consult
98
+ // CORS, so a header naming who may read the endpoint
99
+ // from a web page is a claim with no beneficiary — and
100
+ // on an authenticated endpoint it is better made
101
+ // deliberately than inherited.
102
+ // methods the HTTP verbs this mount actually serves. Two
103
+ // consequences, and the second one is load-bearing:
104
+ // CORS advertises these for the route instead of a
105
+ // fixed five, and listing OPTIONS declares that the
106
+ // mount answers OPTIONS ITSELF — so the global
107
+ // preflight steps aside rather than terminating it.
108
+ // Default null, meaning "ordinary REST verbs, and the
109
+ // preflight is CORS's to answer".
73
110
  // label log prefix. Defaults to `plugin`.
74
111
  // detail optional already-formatted log suffix, e.g.
75
112
  // '(ops=[list,subscribe])'.
@@ -99,6 +136,8 @@ export function registerRoute({
99
136
  plugin,
100
137
  reachability = 'public',
101
138
  streaming = false,
139
+ methods = null,
140
+ cors,
102
141
  label,
103
142
  detail,
104
143
  displayPath,
@@ -112,7 +151,17 @@ export function registerRoute({
112
151
  )
113
152
  }
114
153
 
154
+ if (methods != null && (!Array.isArray(methods) || methods.some(m => typeof m !== 'string'))) {
155
+ throw new Error('registerRoute: `methods` must be an array of verb strings')
156
+ }
157
+
158
+ if (cors !== undefined && cors !== false) {
159
+ throw new Error('registerRoute: `cors` accepts only false — omit it to inherit the global setting')
160
+ }
161
+
115
162
  const descriptor = { path, plugin, reachability, streaming }
163
+ if (methods) descriptor.methods = methods.map(m => m.toUpperCase())
164
+ if (cors === false) descriptor.cors = false
116
165
 
117
166
  // Dedup by path — a re-register (same path) replaces rather than
118
167
  // duplicates. Mounts happen once per process, but this keeps the
package/src/server.js CHANGED
@@ -28,6 +28,7 @@ import { createServer } from 'node:net'
28
28
  import runtime from './runtime.js'
29
29
  import { useLogger } from './engine/index.js'
30
30
  import { onInitialized, onLoad, onLoaded } from './lifecycle.js'
31
+ import { routeFor } from './routes.js'
31
32
 
32
33
  // Every non-loopback IPv4 address this machine answers on.
33
34
  //
@@ -217,11 +218,64 @@ export function setupServer() {
217
218
  // `origin: false` is how the cors package emits no CORS headers at all, which is
218
219
  // what --no-cors / config.server.cors:false asks for.
219
220
  if (!configured) return callback(null, { origin: false })
221
+
222
+ // The mount this request falls inside, if any. Asked per request
223
+ // because routes are registered at onLoaded and this middleware
224
+ // at onLoad — it is mounted before any of them exist, and every
225
+ // request arrives long after they all do.
226
+ const route = routeFor(req.path ?? req.url)
227
+
228
+ // Does that mount answer OPTIONS itself?
229
+ //
230
+ // The cors package defaults to preflightContinue: false, so it
231
+ // ENDS every OPTIONS in the process — 204, CORS headers, no
232
+ // next(). For a browser preflight that is exactly right, and it is
233
+ // what /mcp and /app want: their clients are browsers completing a
234
+ // Streamable HTTP handshake, which is why the mcp plugin pushes
235
+ // mcp-session-id into corsAllowHeaders.
236
+ //
237
+ // WebDAV is not that. The Microsoft WebDAV redirector decides
238
+ // whether a URL is a share at all from the `DAV:` header on
239
+ // OPTIONS — it is DISCOVERY, not a preflight. Answering it here
240
+ // returned 204 with no DAV header and five REST verbs, so Explorer
241
+ // reported ERROR_BAD_NET_NAME (0x80070043) and never even asked
242
+ // for credentials. Every other DAV client works, because Finder,
243
+ // curl, Cyberduck and gvfs do not gate on that header — so this
244
+ // read as "WebDAV is broken on Windows" with the mount logged
245
+ // happily and a 204 that looks like a normal preflight.
246
+ //
247
+ // nephele already computes the right answer and had it thrown
248
+ // away: its OPTIONS builds `DAV: 1, 3, 2` and an Allow list with
249
+ // PROPFIND, LOCK and the rest.
250
+ const ownsPreflight = Boolean(route?.methods?.includes('OPTIONS'))
251
+
252
+ // A route that wants no CORS headers at all.
253
+ //
254
+ // `origin: false` is the cors package's way of emitting none — the
255
+ // same thing --no-cors asks for globally, scoped to one mount.
256
+ //
257
+ // It needs no preflightContinue, and passing one here was dead
258
+ // code: with a falsy origin the package never builds an
259
+ // originCallback, so it calls next() and does not touch the
260
+ // response at all (cors/lib/index.js:210-228). The mount's own
261
+ // OPTIONS is therefore reached whatever preflightContinue says —
262
+ // which is why removing it changed no test, and why the comment
263
+ // that used to claim otherwise was wrong.
264
+ if (route?.cors === false) {
265
+ return callback(null, { origin: false })
266
+ }
267
+
220
268
  callback(null, {
221
269
  origin: configured === true ? '*' : String(configured),
222
- methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
270
+ // The verbs the mount actually serves, when it said. A fixed
271
+ // five advertised DELETE on a read-only mount and hid
272
+ // PROPFIND on a DAV one.
273
+ methods: route?.methods ?? ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
223
274
  allowedHeaders: runtime.options.corsAllowHeaders,
224
275
  exposedHeaders: runtime.options.corsExposeHeaders,
276
+ // Set the CORS headers, then hand the request on to the mount
277
+ // instead of ending it here.
278
+ preflightContinue: ownsPreflight,
225
279
  })
226
280
  }))
227
281
  })