@j0hanz/filesystem-mcp 1.19.0 → 2.0.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/README.md +333 -838
- package/dist/cli-help.d.ts +3 -0
- package/dist/cli-help.d.ts.map +1 -0
- package/dist/cli-help.js +169 -0
- package/dist/cli-help.js.map +1 -0
- package/dist/cli.d.ts +28 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +168 -123
- package/dist/cli.js.map +1 -0
- package/dist/core/concurrency.d.ts +70 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +173 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/config.d.ts +19 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +11 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cursor.d.ts +23 -0
- package/dist/core/cursor.d.ts.map +1 -0
- package/dist/core/cursor.js +62 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/errors.d.ts +96 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +356 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/file-uri.d.ts +24 -0
- package/dist/core/file-uri.d.ts.map +1 -0
- package/dist/core/file-uri.js +52 -0
- package/dist/core/file-uri.js.map +1 -0
- package/dist/core/fmt.d.ts +38 -0
- package/dist/core/fmt.d.ts.map +1 -0
- package/dist/core/fmt.js +109 -0
- package/dist/core/fmt.js.map +1 -0
- package/dist/core/fs.d.ts +108 -0
- package/dist/core/fs.d.ts.map +1 -0
- package/dist/core/fs.js +243 -0
- package/dist/core/fs.js.map +1 -0
- package/dist/core/glob.d.ts +41 -0
- package/dist/core/glob.d.ts.map +1 -0
- package/dist/core/glob.js +399 -0
- package/dist/core/glob.js.map +1 -0
- package/dist/core/input-required.d.ts +147 -0
- package/dist/core/input-required.d.ts.map +1 -0
- package/dist/core/input-required.js +261 -0
- package/dist/core/input-required.js.map +1 -0
- package/dist/core/mime.d.ts +15 -0
- package/dist/core/mime.d.ts.map +1 -0
- package/dist/core/mime.js +188 -0
- package/dist/core/mime.js.map +1 -0
- package/dist/core/observability.d.ts +10 -0
- package/dist/core/observability.d.ts.map +1 -0
- package/dist/core/observability.js +76 -0
- package/dist/core/observability.js.map +1 -0
- package/dist/core/page-store.d.ts +41 -0
- package/dist/core/page-store.d.ts.map +1 -0
- package/dist/core/page-store.js +74 -0
- package/dist/core/page-store.js.map +1 -0
- package/dist/core/path-completer.d.ts +10 -0
- package/dist/core/path-completer.d.ts.map +1 -0
- package/dist/core/path-completer.js +227 -0
- package/dist/core/path-completer.js.map +1 -0
- package/dist/core/path-discovery.d.ts +8 -0
- package/dist/core/path-discovery.d.ts.map +1 -0
- package/dist/core/path-discovery.js +105 -0
- package/dist/core/path-discovery.js.map +1 -0
- package/dist/core/path-utils.d.ts +9 -0
- package/dist/core/path-utils.d.ts.map +1 -0
- package/dist/core/path-utils.js +121 -0
- package/dist/core/path-utils.js.map +1 -0
- package/dist/core/path.d.ts +148 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +674 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/primitives.d.ts +23 -0
- package/dist/core/primitives.d.ts.map +1 -0
- package/dist/core/primitives.js +54 -0
- package/dist/core/primitives.js.map +1 -0
- package/dist/core/read.d.ts +74 -0
- package/dist/core/read.d.ts.map +1 -0
- package/dist/core/read.js +562 -0
- package/dist/core/read.js.map +1 -0
- package/dist/core/schema.d.ts +139 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +345 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/core/search.d.ts +100 -0
- package/dist/core/search.d.ts.map +1 -0
- package/dist/core/search.js +248 -0
- package/dist/core/search.js.map +1 -0
- package/dist/core/sensitive.d.ts +6 -0
- package/dist/core/sensitive.d.ts.map +1 -0
- package/dist/core/sensitive.js +142 -0
- package/dist/core/sensitive.js.map +1 -0
- package/dist/core/store.d.ts +57 -0
- package/dist/core/store.d.ts.map +1 -0
- package/dist/core/store.js +162 -0
- package/dist/core/store.js.map +1 -0
- package/dist/core/util.d.ts +26 -0
- package/dist/core/util.d.ts.map +1 -0
- package/dist/core/util.js +66 -0
- package/dist/core/util.js.map +1 -0
- package/dist/core/watcher-registry.d.ts +68 -0
- package/dist/core/watcher-registry.d.ts.map +1 -0
- package/dist/core/watcher-registry.js +312 -0
- package/dist/core/watcher-registry.js.map +1 -0
- package/dist/http-policy.d.ts +121 -0
- package/dist/http-policy.d.ts.map +1 -0
- package/dist/http-policy.js +340 -0
- package/dist/http-policy.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -41
- package/dist/index.js.map +1 -0
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +71 -0
- package/dist/instructions.js.map +1 -0
- package/dist/prompts.d.ts +8 -6
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +74 -168
- package/dist/prompts.js.map +1 -0
- package/dist/resources.d.ts +73 -9
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +392 -163
- package/dist/resources.js.map +1 -0
- package/dist/server.d.ts +62 -1
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +139 -1
- package/dist/server.js.map +1 -0
- package/dist/tools/batch.d.ts +89 -0
- package/dist/tools/batch.d.ts.map +1 -0
- package/dist/tools/batch.js +157 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/create.d.ts +2 -0
- package/dist/tools/create.d.ts.map +1 -0
- package/dist/tools/create.js +142 -0
- package/dist/tools/create.js.map +1 -0
- package/dist/tools/define.d.ts +110 -0
- package/dist/tools/define.d.ts.map +1 -0
- package/dist/tools/define.js +394 -0
- package/dist/tools/define.js.map +1 -0
- package/dist/tools/delete-file.d.ts +2 -4
- package/dist/tools/delete-file.d.ts.map +1 -0
- package/dist/tools/delete-file.js +330 -87
- package/dist/tools/delete-file.js.map +1 -0
- package/dist/tools/diff.d.ts +2 -0
- package/dist/tools/diff.d.ts.map +1 -0
- package/dist/tools/diff.js +94 -0
- package/dist/tools/diff.js.map +1 -0
- package/dist/tools/edit.d.ts +2 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +442 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/index.d.ts +26 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/list.d.ts +2 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +291 -0
- package/dist/tools/list.js.map +1 -0
- package/dist/tools/move.d.ts +2 -0
- package/dist/tools/move.d.ts.map +1 -0
- package/dist/tools/move.js +321 -0
- package/dist/tools/move.js.map +1 -0
- package/dist/tools/patch.d.ts +2 -0
- package/dist/tools/patch.d.ts.map +1 -0
- package/dist/tools/patch.js +155 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/progress.d.ts +52 -0
- package/dist/tools/progress.d.ts.map +1 -0
- package/dist/tools/progress.js +172 -0
- package/dist/tools/progress.js.map +1 -0
- package/dist/tools/read.d.ts +2 -4
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +398 -152
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/replace-in-files.d.ts +2 -4
- package/dist/tools/replace-in-files.d.ts.map +1 -0
- package/dist/tools/replace-in-files.js +435 -264
- package/dist/tools/replace-in-files.js.map +1 -0
- package/dist/tools/roots.d.ts +2 -4
- package/dist/tools/roots.d.ts.map +1 -0
- package/dist/tools/roots.js +29 -52
- package/dist/tools/roots.js.map +1 -0
- package/dist/tools/search-content.d.ts +2 -4
- package/dist/tools/search-content.d.ts.map +1 -0
- package/dist/tools/search-content.js +262 -228
- package/dist/tools/search-content.js.map +1 -0
- package/dist/tools/search-files.d.ts +2 -4
- package/dist/tools/search-files.d.ts.map +1 -0
- package/dist/tools/search-files.js +162 -161
- package/dist/tools/search-files.js.map +1 -0
- package/dist/tools/stat.d.ts +2 -4
- package/dist/tools/stat.d.ts.map +1 -0
- package/dist/tools/stat.js +202 -62
- package/dist/tools/stat.js.map +1 -0
- package/dist/transport/http.d.ts +5 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +276 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/shared.d.ts +45 -0
- package/dist/transport/shared.d.ts.map +1 -0
- package/dist/transport/shared.js +65 -0
- package/dist/transport/shared.js.map +1 -0
- package/dist/transport/stdio.d.ts +32 -0
- package/dist/transport/stdio.d.ts.map +1 -0
- package/dist/transport/stdio.js +279 -0
- package/dist/transport/stdio.js.map +1 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +4 -0
- package/dist/transport.js.map +1 -0
- package/package.json +37 -46
- package/dist/assets/logo.svg +0 -35
- package/dist/completions.d.ts +0 -2
- package/dist/completions.js +0 -572
- package/dist/config.d.ts +0 -119
- package/dist/config.js +0 -31
- package/dist/lib/abort.d.ts +0 -7
- package/dist/lib/abort.js +0 -81
- package/dist/lib/constants.d.ts +0 -42
- package/dist/lib/constants.js +0 -313
- package/dist/lib/errors.d.ts +0 -28
- package/dist/lib/errors.js +0 -301
- package/dist/lib/file-operations/core.d.ts +0 -53
- package/dist/lib/file-operations/core.js +0 -144
- package/dist/lib/file-operations/metadata.d.ts +0 -77
- package/dist/lib/file-operations/metadata.js +0 -878
- package/dist/lib/file-operations/search.d.ts +0 -46
- package/dist/lib/file-operations/search.js +0 -1194
- package/dist/lib/file-operations/traversal.d.ts +0 -40
- package/dist/lib/file-operations/traversal.js +0 -310
- package/dist/lib/fs-helpers.d.ts +0 -46
- package/dist/lib/fs-helpers.js +0 -606
- package/dist/lib/globs.d.ts +0 -2
- package/dist/lib/globs.js +0 -19
- package/dist/lib/logger.d.ts +0 -27
- package/dist/lib/logger.js +0 -91
- package/dist/lib/observability.d.ts +0 -33
- package/dist/lib/observability.js +0 -373
- package/dist/lib/paths.d.ts +0 -35
- package/dist/lib/paths.js +0 -634
- package/dist/lib/resource-store.d.ts +0 -28
- package/dist/lib/resource-store.js +0 -195
- package/dist/lib/utils.d.ts +0 -7
- package/dist/lib/utils.js +0 -37
- package/dist/lib/zod-codecs.d.ts +0 -2
- package/dist/lib/zod-codecs.js +0 -18
- package/dist/pkg-info.d.ts +0 -7
- package/dist/pkg-info.js +0 -9
- package/dist/resources/generated-instructions.d.ts +0 -1
- package/dist/resources/generated-instructions.js +0 -105
- package/dist/resources/tool-catalog.d.ts +0 -2
- package/dist/resources/tool-catalog.js +0 -78
- package/dist/resources/tool-info.d.ts +0 -9
- package/dist/resources/tool-info.js +0 -206
- package/dist/resources/workflows.d.ts +0 -1
- package/dist/resources/workflows.js +0 -72
- package/dist/schemas.d.ts +0 -505
- package/dist/schemas.js +0 -676
- package/dist/server/bootstrap.d.ts +0 -6
- package/dist/server/bootstrap.js +0 -563
- package/dist/server/event-store.d.ts +0 -18
- package/dist/server/event-store.js +0 -71
- package/dist/server/roots-manager.d.ts +0 -28
- package/dist/server/roots-manager.js +0 -227
- package/dist/server/task-store.d.ts +0 -10
- package/dist/server/task-store.js +0 -91
- package/dist/tools/apply-patch.d.ts +0 -4
- package/dist/tools/apply-patch.js +0 -235
- package/dist/tools/calculate-hash.d.ts +0 -4
- package/dist/tools/calculate-hash.js +0 -187
- package/dist/tools/contract.d.ts +0 -50
- package/dist/tools/contract.js +0 -1
- package/dist/tools/create-directory.d.ts +0 -4
- package/dist/tools/create-directory.js +0 -68
- package/dist/tools/diff-files.d.ts +0 -4
- package/dist/tools/diff-files.js +0 -136
- package/dist/tools/edit-file.d.ts +0 -4
- package/dist/tools/edit-file.js +0 -256
- package/dist/tools/icons.d.ts +0 -15
- package/dist/tools/icons.js +0 -24
- package/dist/tools/list-directory.d.ts +0 -4
- package/dist/tools/list-directory.js +0 -218
- package/dist/tools/move-file.d.ts +0 -4
- package/dist/tools/move-file.js +0 -178
- package/dist/tools/read-multiple.d.ts +0 -4
- package/dist/tools/read-multiple.js +0 -207
- package/dist/tools/shared.d.ts +0 -172
- package/dist/tools/shared.js +0 -599
- package/dist/tools/stat-many.d.ts +0 -4
- package/dist/tools/stat-many.js +0 -104
- package/dist/tools/task-support.d.ts +0 -29
- package/dist/tools/task-support.js +0 -472
- package/dist/tools/tree.d.ts +0 -4
- package/dist/tools/tree.js +0 -102
- package/dist/tools/write-file.d.ts +0 -4
- package/dist/tools/write-file.js +0 -60
- package/dist/tools.d.ts +0 -5
- package/dist/tools.js +0 -54
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { statSync, watch } from 'node:fs';
|
|
2
|
+
import { formatUnknownErrorMessage } from './errors.js';
|
|
3
|
+
import { extractPath } from './file-uri.js';
|
|
4
|
+
import { Logger } from './observability.js';
|
|
5
|
+
import { parseEnvInt } from './util.js';
|
|
6
|
+
// Cap concurrent file watchers to avoid exhausting OS-level watch handles
|
|
7
|
+
// (e.g. Linux inotify, default ~8192/user). One subscription == one watcher.
|
|
8
|
+
export const MAX_WATCHERS = parseEnvInt('FS_MAX_WATCHERS', 256, 1, 4096);
|
|
9
|
+
function warnWatcherCap(uri) {
|
|
10
|
+
Logger.warn(`Cannot subscribe to ${uri}: MAX_WATCHERS limit (${MAX_WATCHERS}) reached.`);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Owns the uri → FSWatcher map and the subscription bookkeeping around it:
|
|
14
|
+
* notify callbacks, desired subscribe/unsubscribe state, and the watcher cap —
|
|
15
|
+
* plus `acquire`, the one ladder that sequences them correctly. `acquire` awaits
|
|
16
|
+
* path validation midway and re-checks `isStale` and `hasWatcher` itself
|
|
17
|
+
* afterwards, so callers do not: they take a lease and release it. The
|
|
18
|
+
* individual primitives stay public for the state-machine tests.
|
|
19
|
+
*/
|
|
20
|
+
export function createWatcherRegistry() {
|
|
21
|
+
const watchers = new Map();
|
|
22
|
+
// A URI may have several notification sinks and several independent leases.
|
|
23
|
+
// Callback identity and lifetime are intentionally separate: the HTTP leg
|
|
24
|
+
// uses one stable bus-publishing callback for every listen stream, while each
|
|
25
|
+
// stream still owns one lease that must be released on close.
|
|
26
|
+
// ponytail: no per-subscription id in the SDK unsubscribe contract, so we
|
|
27
|
+
// ref-count by URI. A departed direct-notification callback can linger until
|
|
28
|
+
// the final lease ends; sends to a closed transport already fail harmlessly.
|
|
29
|
+
// The count outlives the watcher (see `dropWatcher`), so a URI whose watcher
|
|
30
|
+
// errored keeps one map entry per still-unreleased lease until they drain —
|
|
31
|
+
// no watcher slot, just bookkeeping. Per-subscription-id keying retires both
|
|
32
|
+
// this and the unsubscribe-by-URI over-release if that churn is observed.
|
|
33
|
+
const activeCallbacks = new Map();
|
|
34
|
+
const subscriberCounts = new Map();
|
|
35
|
+
const desiredState = new Map();
|
|
36
|
+
const debounceTimers = new Map();
|
|
37
|
+
let destroyed = false;
|
|
38
|
+
// 'unsubscribed' exists only to abort a subscribe that is mid-await
|
|
39
|
+
// (isStale). If no subscribe is in flight, keep no entry at all: a lingering
|
|
40
|
+
// 'unsubscribed' would permanently block the modern attach path (which never
|
|
41
|
+
// calls startSubscribe) and leak one map entry per URI ever watched. This
|
|
42
|
+
// must be idempotent: remove()'s teardown branch calls this directly and
|
|
43
|
+
// then unconditionally calls dropWatcher (which also calls this) for the
|
|
44
|
+
// same uri — the second call must not clobber what the first one set.
|
|
45
|
+
const settleDesiredState = (uri) => {
|
|
46
|
+
const current = desiredState.get(uri);
|
|
47
|
+
if (current === 'subscribing') {
|
|
48
|
+
desiredState.set(uri, 'unsubscribed');
|
|
49
|
+
}
|
|
50
|
+
else if (current !== 'unsubscribed') {
|
|
51
|
+
desiredState.delete(uri);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
// Tears down the watcher itself, NOT the ref-count. The 'error' event calls
|
|
55
|
+
// this with leases still outstanding: clearing the count there would let the
|
|
56
|
+
// next release from one of those leases decrement from zero and drop a
|
|
57
|
+
// watcher a later subscriber re-established for the same URI. `drop` below is
|
|
58
|
+
// the only path that ends leases, and it is the only one that clears them.
|
|
59
|
+
const dropWatcher = (uri, watcher) => {
|
|
60
|
+
const current = watchers.get(uri);
|
|
61
|
+
if (current !== watcher)
|
|
62
|
+
return;
|
|
63
|
+
const timer = debounceTimers.get(uri);
|
|
64
|
+
if (timer) {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
debounceTimers.delete(uri);
|
|
67
|
+
}
|
|
68
|
+
watcher.close();
|
|
69
|
+
watchers.delete(uri);
|
|
70
|
+
activeCallbacks.delete(uri);
|
|
71
|
+
settleDesiredState(uri);
|
|
72
|
+
};
|
|
73
|
+
/** The last lease ended: drop the watcher and every trace of the URI. */
|
|
74
|
+
const drop = (uri) => {
|
|
75
|
+
const timer = debounceTimers.get(uri);
|
|
76
|
+
if (timer) {
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
debounceTimers.delete(uri);
|
|
79
|
+
}
|
|
80
|
+
const watcher = watchers.get(uri);
|
|
81
|
+
if (watcher)
|
|
82
|
+
dropWatcher(uri, watcher);
|
|
83
|
+
activeCallbacks.delete(uri);
|
|
84
|
+
subscriberCounts.delete(uri);
|
|
85
|
+
settleDesiredState(uri);
|
|
86
|
+
};
|
|
87
|
+
const notifyAll = (uri) => {
|
|
88
|
+
const callbacks = activeCallbacks.get(uri);
|
|
89
|
+
if (!callbacks || callbacks.size === 0)
|
|
90
|
+
return;
|
|
91
|
+
const existing = debounceTimers.get(uri);
|
|
92
|
+
if (existing) {
|
|
93
|
+
clearTimeout(existing);
|
|
94
|
+
}
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
debounceTimers.delete(uri);
|
|
97
|
+
const cbs = activeCallbacks.get(uri);
|
|
98
|
+
if (!cbs)
|
|
99
|
+
return;
|
|
100
|
+
// One debounce timer per URI — fan out to every subscriber inside it, so
|
|
101
|
+
// a burst of changes still fires one notification round per URI, not per
|
|
102
|
+
// subscriber.
|
|
103
|
+
for (const cb of cbs) {
|
|
104
|
+
try {
|
|
105
|
+
cb(uri);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
Logger.warn(`Notify callback error for ${uri}: ${formatUnknownErrorMessage(err)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}, 50);
|
|
112
|
+
timer.unref();
|
|
113
|
+
debounceTimers.set(uri, timer);
|
|
114
|
+
};
|
|
115
|
+
// The seven members below are hoisted out of the returned literal so `acquire`
|
|
116
|
+
// can sequence them from inside the closure. Their signatures and the shape of
|
|
117
|
+
// the returned object are unchanged — they are still public members, listed
|
|
118
|
+
// shorthand in the `return` at the bottom.
|
|
119
|
+
const hasWatcher = (uri) => watchers.has(uri);
|
|
120
|
+
const isAtCap = () => watchers.size >= MAX_WATCHERS;
|
|
121
|
+
/** The registry was destroyed, or this uri was unsubscribed, mid-await. */
|
|
122
|
+
const isStale = (uri) => destroyed || desiredState.get(uri) === 'unsubscribed';
|
|
123
|
+
const startSubscribe = (uri) => {
|
|
124
|
+
desiredState.set(uri, 'subscribing');
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* A subscribe that declared intent and then failed — bad URI, unvalidatable
|
|
128
|
+
* path, cap, fs.watch refusal — drops the entry entirely. Not
|
|
129
|
+
* `settleDesiredState`: that maps 'subscribing' onto 'unsubscribed' to abort
|
|
130
|
+
* an attach still mid-await, and a *finished* failure leaving 'unsubscribed'
|
|
131
|
+
* behind would make `isStale` true forever, so the modern attach path (which
|
|
132
|
+
* never calls `startSubscribe`) could never watch this uri again. Only
|
|
133
|
+
* clears its own 'subscribing' — a concurrent unsubscribe that already set
|
|
134
|
+
* 'unsubscribed' still wins.
|
|
135
|
+
*/
|
|
136
|
+
const cancelSubscribe = (uri) => {
|
|
137
|
+
if (desiredState.get(uri) === 'subscribing')
|
|
138
|
+
desiredState.delete(uri);
|
|
139
|
+
};
|
|
140
|
+
const addCallback = (uri, notify) => {
|
|
141
|
+
let callbacks = activeCallbacks.get(uri);
|
|
142
|
+
if (!callbacks) {
|
|
143
|
+
callbacks = new Set();
|
|
144
|
+
activeCallbacks.set(uri, callbacks);
|
|
145
|
+
}
|
|
146
|
+
callbacks.add(notify);
|
|
147
|
+
desiredState.set(uri, 'subscribed');
|
|
148
|
+
};
|
|
149
|
+
const retain = (uri) => {
|
|
150
|
+
subscriberCounts.set(uri, (subscriberCounts.get(uri) ?? 0) + 1);
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Ref-count by URI: only tear down the shared watcher when the last lease
|
|
154
|
+
* ends. Callback identity is independent from this count. A release with no
|
|
155
|
+
* lease outstanding (a rolled-back attach) drops the watcher outright.
|
|
156
|
+
*/
|
|
157
|
+
const release = (uri) => {
|
|
158
|
+
const remaining = (subscriberCounts.get(uri) ?? 0) - 1;
|
|
159
|
+
if (remaining > 0) {
|
|
160
|
+
subscriberCounts.set(uri, remaining);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
drop(uri);
|
|
164
|
+
};
|
|
165
|
+
const attach = (uri, resolvedPath) => {
|
|
166
|
+
try {
|
|
167
|
+
// Watch directories recursively (children included) and files as-is.
|
|
168
|
+
// `fs.watch` async errors arrive via the 'error' event below, not as a
|
|
169
|
+
// sync throw, so no recursive-fallback try/catch is needed here — the
|
|
170
|
+
// outer catch handles sync throws (inotify exhaustion, path-race).
|
|
171
|
+
// `{ recursive: true }` is honored on macOS, Windows, and — since Node
|
|
172
|
+
// 20.13 — Linux; `engines.node` is >=24, so all three are covered.
|
|
173
|
+
const recursive = statSync(resolvedPath).isDirectory();
|
|
174
|
+
const watcher = watch(resolvedPath, recursive ? { recursive: true } : undefined, () => {
|
|
175
|
+
notifyAll(uri);
|
|
176
|
+
});
|
|
177
|
+
watcher.on('error', (err) => {
|
|
178
|
+
Logger.warn(`Watcher error for ${uri}: ${err.message}`);
|
|
179
|
+
dropWatcher(uri, watcher);
|
|
180
|
+
});
|
|
181
|
+
// Two attaches that both cleared `hasWatcher` before either finished
|
|
182
|
+
// validating land here for the same uri. Keep the one already wired to
|
|
183
|
+
// the callback set and close this one — overwriting the map entry would
|
|
184
|
+
// strand the first watcher's fd with nothing left holding a reference.
|
|
185
|
+
if (watchers.has(uri)) {
|
|
186
|
+
watcher.close();
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
watchers.set(uri, watcher);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
Logger.error(`Failed to create watcher for ${uri}: ${formatUnknownErrorMessage(err)}`);
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
return {
|
|
198
|
+
hasWatcher,
|
|
199
|
+
isAtCap,
|
|
200
|
+
/** Live watcher count, for pre-checking a batched listen against remaining capacity. */
|
|
201
|
+
size: () => watchers.size,
|
|
202
|
+
isStale,
|
|
203
|
+
startSubscribe,
|
|
204
|
+
cancelSubscribe,
|
|
205
|
+
addCallback,
|
|
206
|
+
retain,
|
|
207
|
+
release,
|
|
208
|
+
/**
|
|
209
|
+
* The one attach ladder both watcher entry points run: `resources/subscribe`
|
|
210
|
+
* (2025 era) and the `subscriptions/listen` filter (modern era, HTTP and
|
|
211
|
+
* stdio). Never throws — it reports the outcome and lets each caller decide
|
|
212
|
+
* what that is worth: subscribe owes its caller a precise error, and listen
|
|
213
|
+
* (`prepareListenWatchers`) treats the batch as all-or-nothing, releasing
|
|
214
|
+
* every lease it already took and rejecting the request. Idempotent per URI
|
|
215
|
+
* — a second call for an already-watched URI re-registers the notify
|
|
216
|
+
* callback (one watcher per URI).
|
|
217
|
+
*
|
|
218
|
+
* `markSubscribe` is the one branch that differs: only `resources/subscribe`
|
|
219
|
+
* declares desired state (what `isStale` aborts against). The listen path
|
|
220
|
+
* must not, or a rejected attach past that point strands a `'subscribing'`
|
|
221
|
+
* entry nothing settles. Whoever declares it, this function settles it:
|
|
222
|
+
* every failing exit past that point cancels the declaration, so no uri is
|
|
223
|
+
* poisoned for a later attach.
|
|
224
|
+
*
|
|
225
|
+
* Every `ok` return takes one lease; lifetime is the caller's to manage.
|
|
226
|
+
* HTTP releases when the listen response closes. Stdio tracks the listen
|
|
227
|
+
* request ID and releases on cancellation, SDK rejection, or graceful
|
|
228
|
+
* completion. In both legs the ref-count is by URI, so a watcher another
|
|
229
|
+
* stream still holds survives.
|
|
230
|
+
*/
|
|
231
|
+
async acquire(pathGuard, uri, notify, { markSubscribe = false } = {}) {
|
|
232
|
+
// Every failing exit below routes through here, so the declaration made by
|
|
233
|
+
// `startSubscribe` can never outlive the attach that made it.
|
|
234
|
+
const fail = (result) => {
|
|
235
|
+
if (markSubscribe)
|
|
236
|
+
cancelSubscribe(uri);
|
|
237
|
+
return result;
|
|
238
|
+
};
|
|
239
|
+
if (hasWatcher(uri)) {
|
|
240
|
+
// A watcher already tracks this uri; just (re)register the callback so
|
|
241
|
+
// its change events reach the new subscriber. No validation or cap work
|
|
242
|
+
// is needed for an already-live watcher.
|
|
243
|
+
addCallback(uri, notify);
|
|
244
|
+
retain(uri);
|
|
245
|
+
return { ok: true };
|
|
246
|
+
}
|
|
247
|
+
// A cap hit before validation and one found after the await are the same
|
|
248
|
+
// condition, and both are reported the same way.
|
|
249
|
+
if (isAtCap()) {
|
|
250
|
+
warnWatcherCap(uri);
|
|
251
|
+
return { ok: false, reason: 'capped' };
|
|
252
|
+
}
|
|
253
|
+
if (markSubscribe)
|
|
254
|
+
startSubscribe(uri);
|
|
255
|
+
const filePath = extractPath(uri);
|
|
256
|
+
if (!filePath)
|
|
257
|
+
return fail({ ok: false, reason: 'bad-uri' });
|
|
258
|
+
let resolved;
|
|
259
|
+
try {
|
|
260
|
+
resolved = await pathGuard.validateExistingPath(filePath);
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
return fail({ ok: false, reason: 'invalid-path', error });
|
|
264
|
+
}
|
|
265
|
+
// Re-check what the await could have changed. A stale uri is the one
|
|
266
|
+
// failure that must NOT cancel: an unsubscribe landed mid-await and its
|
|
267
|
+
// 'unsubscribed' marker is the thing that aborted this attach.
|
|
268
|
+
if (isStale(uri))
|
|
269
|
+
return { ok: false, reason: 'stale' };
|
|
270
|
+
if (hasWatcher(uri)) {
|
|
271
|
+
addCallback(uri, notify);
|
|
272
|
+
retain(uri);
|
|
273
|
+
return { ok: true };
|
|
274
|
+
}
|
|
275
|
+
if (isAtCap()) {
|
|
276
|
+
warnWatcherCap(uri);
|
|
277
|
+
return fail({ ok: false, reason: 'capped' });
|
|
278
|
+
}
|
|
279
|
+
addCallback(uri, notify);
|
|
280
|
+
if (!attach(uri, resolved)) {
|
|
281
|
+
// fs.watch threw (inotify exhaustion, or a race deleted the path): roll
|
|
282
|
+
// back so no dangling callback is left believing a watcher exists. No
|
|
283
|
+
// lease was taken yet, so `release` drops the entry outright.
|
|
284
|
+
release(uri);
|
|
285
|
+
return fail({ ok: false, reason: 'attach-failed' });
|
|
286
|
+
}
|
|
287
|
+
retain(uri);
|
|
288
|
+
return { ok: true };
|
|
289
|
+
},
|
|
290
|
+
attach,
|
|
291
|
+
destroy() {
|
|
292
|
+
destroyed = true;
|
|
293
|
+
for (const timer of debounceTimers.values()) {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
}
|
|
296
|
+
debounceTimers.clear();
|
|
297
|
+
for (const watcher of watchers.values()) {
|
|
298
|
+
try {
|
|
299
|
+
watcher.close();
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
/* ignore close errors so all watchers are attempted */
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
watchers.clear();
|
|
306
|
+
activeCallbacks.clear();
|
|
307
|
+
subscriberCounts.clear();
|
|
308
|
+
desiredState.clear();
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
//# sourceMappingURL=watcher-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher-registry.js","sourceRoot":"","sources":["../../src/core/watcher-registry.ts"],"names":[],"mappings":"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","sourcesContent":["import type { FSWatcher } from 'node:fs';\nimport { statSync, watch } from 'node:fs';\n\nimport { formatUnknownErrorMessage } from './errors.js';\nimport { extractPath } from './file-uri.js';\nimport { Logger } from './observability.js';\nimport type { PathGuard } from './path.js';\nimport { parseEnvInt } from './util.js';\n\n// Cap concurrent file watchers to avoid exhausting OS-level watch handles\n// (e.g. Linux inotify, default ~8192/user). One subscription == one watcher.\nexport const MAX_WATCHERS = parseEnvInt('FS_MAX_WATCHERS', 256, 1, 4096);\n\nfunction warnWatcherCap(uri: string): void {\n Logger.warn(`Cannot subscribe to ${uri}: MAX_WATCHERS limit (${MAX_WATCHERS}) reached.`);\n}\n\n/**\n * `ok` means a watcher is live for this uri and `notify` is registered. Every\n * other outcome names why, and only `invalid-path` carries what\n * `validateExistingPath` threw — so `error` is unreachable on a branch that has\n * none.\n */\nexport type WatcherAttachResult =\n | { ok: true }\n | { ok: false; reason: 'stale' | 'capped' | 'bad-uri' | 'attach-failed' }\n | { ok: false; reason: 'invalid-path'; error: unknown };\n\n/**\n * Owns the uri → FSWatcher map and the subscription bookkeeping around it:\n * notify callbacks, desired subscribe/unsubscribe state, and the watcher cap —\n * plus `acquire`, the one ladder that sequences them correctly. `acquire` awaits\n * path validation midway and re-checks `isStale` and `hasWatcher` itself\n * afterwards, so callers do not: they take a lease and release it. The\n * individual primitives stay public for the state-machine tests.\n */\nexport function createWatcherRegistry() {\n const watchers = new Map<string, FSWatcher>();\n // A URI may have several notification sinks and several independent leases.\n // Callback identity and lifetime are intentionally separate: the HTTP leg\n // uses one stable bus-publishing callback for every listen stream, while each\n // stream still owns one lease that must be released on close.\n // ponytail: no per-subscription id in the SDK unsubscribe contract, so we\n // ref-count by URI. A departed direct-notification callback can linger until\n // the final lease ends; sends to a closed transport already fail harmlessly.\n // The count outlives the watcher (see `dropWatcher`), so a URI whose watcher\n // errored keeps one map entry per still-unreleased lease until they drain —\n // no watcher slot, just bookkeeping. Per-subscription-id keying retires both\n // this and the unsubscribe-by-URI over-release if that churn is observed.\n const activeCallbacks = new Map<string, Set<(uri: string) => void>>();\n const subscriberCounts = new Map<string, number>();\n const desiredState = new Map<string, 'subscribed' | 'unsubscribed' | 'subscribing'>();\n const debounceTimers = new Map<string, NodeJS.Timeout>();\n let destroyed = false;\n\n // 'unsubscribed' exists only to abort a subscribe that is mid-await\n // (isStale). If no subscribe is in flight, keep no entry at all: a lingering\n // 'unsubscribed' would permanently block the modern attach path (which never\n // calls startSubscribe) and leak one map entry per URI ever watched. This\n // must be idempotent: remove()'s teardown branch calls this directly and\n // then unconditionally calls dropWatcher (which also calls this) for the\n // same uri — the second call must not clobber what the first one set.\n const settleDesiredState = (uri: string): void => {\n const current = desiredState.get(uri);\n if (current === 'subscribing') {\n desiredState.set(uri, 'unsubscribed');\n } else if (current !== 'unsubscribed') {\n desiredState.delete(uri);\n }\n };\n\n // Tears down the watcher itself, NOT the ref-count. The 'error' event calls\n // this with leases still outstanding: clearing the count there would let the\n // next release from one of those leases decrement from zero and drop a\n // watcher a later subscriber re-established for the same URI. `drop` below is\n // the only path that ends leases, and it is the only one that clears them.\n const dropWatcher = (uri: string, watcher: FSWatcher): void => {\n const current = watchers.get(uri);\n if (current !== watcher) return;\n const timer = debounceTimers.get(uri);\n if (timer) {\n clearTimeout(timer);\n debounceTimers.delete(uri);\n }\n watcher.close();\n watchers.delete(uri);\n activeCallbacks.delete(uri);\n settleDesiredState(uri);\n };\n\n /** The last lease ended: drop the watcher and every trace of the URI. */\n const drop = (uri: string): void => {\n const timer = debounceTimers.get(uri);\n if (timer) {\n clearTimeout(timer);\n debounceTimers.delete(uri);\n }\n const watcher = watchers.get(uri);\n if (watcher) dropWatcher(uri, watcher);\n activeCallbacks.delete(uri);\n subscriberCounts.delete(uri);\n settleDesiredState(uri);\n };\n\n const notifyAll = (uri: string): void => {\n const callbacks = activeCallbacks.get(uri);\n if (!callbacks || callbacks.size === 0) return;\n const existing = debounceTimers.get(uri);\n if (existing) {\n clearTimeout(existing);\n }\n const timer = setTimeout(() => {\n debounceTimers.delete(uri);\n const cbs = activeCallbacks.get(uri);\n if (!cbs) return;\n // One debounce timer per URI — fan out to every subscriber inside it, so\n // a burst of changes still fires one notification round per URI, not per\n // subscriber.\n for (const cb of cbs) {\n try {\n cb(uri);\n } catch (err) {\n Logger.warn(`Notify callback error for ${uri}: ${formatUnknownErrorMessage(err)}`);\n }\n }\n }, 50);\n timer.unref();\n debounceTimers.set(uri, timer);\n };\n\n // The seven members below are hoisted out of the returned literal so `acquire`\n // can sequence them from inside the closure. Their signatures and the shape of\n // the returned object are unchanged — they are still public members, listed\n // shorthand in the `return` at the bottom.\n\n const hasWatcher = (uri: string): boolean => watchers.has(uri);\n\n const isAtCap = (): boolean => watchers.size >= MAX_WATCHERS;\n\n /** The registry was destroyed, or this uri was unsubscribed, mid-await. */\n const isStale = (uri: string): boolean => destroyed || desiredState.get(uri) === 'unsubscribed';\n\n const startSubscribe = (uri: string): void => {\n desiredState.set(uri, 'subscribing');\n };\n\n /**\n * A subscribe that declared intent and then failed — bad URI, unvalidatable\n * path, cap, fs.watch refusal — drops the entry entirely. Not\n * `settleDesiredState`: that maps 'subscribing' onto 'unsubscribed' to abort\n * an attach still mid-await, and a *finished* failure leaving 'unsubscribed'\n * behind would make `isStale` true forever, so the modern attach path (which\n * never calls `startSubscribe`) could never watch this uri again. Only\n * clears its own 'subscribing' — a concurrent unsubscribe that already set\n * 'unsubscribed' still wins.\n */\n const cancelSubscribe = (uri: string): void => {\n if (desiredState.get(uri) === 'subscribing') desiredState.delete(uri);\n };\n\n const addCallback = (uri: string, notify: (uri: string) => void): void => {\n let callbacks = activeCallbacks.get(uri);\n if (!callbacks) {\n callbacks = new Set();\n activeCallbacks.set(uri, callbacks);\n }\n callbacks.add(notify);\n desiredState.set(uri, 'subscribed');\n };\n\n const retain = (uri: string): void => {\n subscriberCounts.set(uri, (subscriberCounts.get(uri) ?? 0) + 1);\n };\n\n /**\n * Ref-count by URI: only tear down the shared watcher when the last lease\n * ends. Callback identity is independent from this count. A release with no\n * lease outstanding (a rolled-back attach) drops the watcher outright.\n */\n const release = (uri: string): void => {\n const remaining = (subscriberCounts.get(uri) ?? 0) - 1;\n if (remaining > 0) {\n subscriberCounts.set(uri, remaining);\n return;\n }\n drop(uri);\n };\n\n const attach = (uri: string, resolvedPath: string): boolean => {\n try {\n // Watch directories recursively (children included) and files as-is.\n // `fs.watch` async errors arrive via the 'error' event below, not as a\n // sync throw, so no recursive-fallback try/catch is needed here — the\n // outer catch handles sync throws (inotify exhaustion, path-race).\n // `{ recursive: true }` is honored on macOS, Windows, and — since Node\n // 20.13 — Linux; `engines.node` is >=24, so all three are covered.\n const recursive = statSync(resolvedPath).isDirectory();\n const watcher = watch(resolvedPath, recursive ? { recursive: true } : undefined, () => {\n notifyAll(uri);\n });\n watcher.on('error', (err: Error) => {\n Logger.warn(`Watcher error for ${uri}: ${err.message}`);\n dropWatcher(uri, watcher);\n });\n // Two attaches that both cleared `hasWatcher` before either finished\n // validating land here for the same uri. Keep the one already wired to\n // the callback set and close this one — overwriting the map entry would\n // strand the first watcher's fd with nothing left holding a reference.\n if (watchers.has(uri)) {\n watcher.close();\n return true;\n }\n watchers.set(uri, watcher);\n return true;\n } catch (err) {\n Logger.error(`Failed to create watcher for ${uri}: ${formatUnknownErrorMessage(err)}`);\n return false;\n }\n };\n\n return {\n hasWatcher,\n\n isAtCap,\n\n /** Live watcher count, for pre-checking a batched listen against remaining capacity. */\n size: (): number => watchers.size,\n\n isStale,\n\n startSubscribe,\n\n cancelSubscribe,\n\n addCallback,\n\n retain,\n\n release,\n\n /**\n * The one attach ladder both watcher entry points run: `resources/subscribe`\n * (2025 era) and the `subscriptions/listen` filter (modern era, HTTP and\n * stdio). Never throws — it reports the outcome and lets each caller decide\n * what that is worth: subscribe owes its caller a precise error, and listen\n * (`prepareListenWatchers`) treats the batch as all-or-nothing, releasing\n * every lease it already took and rejecting the request. Idempotent per URI\n * — a second call for an already-watched URI re-registers the notify\n * callback (one watcher per URI).\n *\n * `markSubscribe` is the one branch that differs: only `resources/subscribe`\n * declares desired state (what `isStale` aborts against). The listen path\n * must not, or a rejected attach past that point strands a `'subscribing'`\n * entry nothing settles. Whoever declares it, this function settles it:\n * every failing exit past that point cancels the declaration, so no uri is\n * poisoned for a later attach.\n *\n * Every `ok` return takes one lease; lifetime is the caller's to manage.\n * HTTP releases when the listen response closes. Stdio tracks the listen\n * request ID and releases on cancellation, SDK rejection, or graceful\n * completion. In both legs the ref-count is by URI, so a watcher another\n * stream still holds survives.\n */\n async acquire(\n pathGuard: PathGuard,\n uri: string,\n notify: (uri: string) => void,\n { markSubscribe = false }: { markSubscribe?: boolean } = {},\n ): Promise<WatcherAttachResult> {\n // Every failing exit below routes through here, so the declaration made by\n // `startSubscribe` can never outlive the attach that made it.\n const fail = (result: WatcherAttachResult & { ok: false }): WatcherAttachResult => {\n if (markSubscribe) cancelSubscribe(uri);\n return result;\n };\n\n if (hasWatcher(uri)) {\n // A watcher already tracks this uri; just (re)register the callback so\n // its change events reach the new subscriber. No validation or cap work\n // is needed for an already-live watcher.\n addCallback(uri, notify);\n retain(uri);\n return { ok: true };\n }\n // A cap hit before validation and one found after the await are the same\n // condition, and both are reported the same way.\n if (isAtCap()) {\n warnWatcherCap(uri);\n return { ok: false, reason: 'capped' };\n }\n\n if (markSubscribe) startSubscribe(uri);\n\n const filePath = extractPath(uri);\n if (!filePath) return fail({ ok: false, reason: 'bad-uri' });\n\n let resolved: string;\n try {\n resolved = await pathGuard.validateExistingPath(filePath);\n } catch (error: unknown) {\n return fail({ ok: false, reason: 'invalid-path', error });\n }\n\n // Re-check what the await could have changed. A stale uri is the one\n // failure that must NOT cancel: an unsubscribe landed mid-await and its\n // 'unsubscribed' marker is the thing that aborted this attach.\n if (isStale(uri)) return { ok: false, reason: 'stale' };\n if (hasWatcher(uri)) {\n addCallback(uri, notify);\n retain(uri);\n return { ok: true };\n }\n if (isAtCap()) {\n warnWatcherCap(uri);\n return fail({ ok: false, reason: 'capped' });\n }\n\n addCallback(uri, notify);\n if (!attach(uri, resolved)) {\n // fs.watch threw (inotify exhaustion, or a race deleted the path): roll\n // back so no dangling callback is left believing a watcher exists. No\n // lease was taken yet, so `release` drops the entry outright.\n release(uri);\n return fail({ ok: false, reason: 'attach-failed' });\n }\n retain(uri);\n return { ok: true };\n },\n\n attach,\n\n destroy(): void {\n destroyed = true;\n for (const timer of debounceTimers.values()) {\n clearTimeout(timer);\n }\n debounceTimers.clear();\n for (const watcher of watchers.values()) {\n try {\n watcher.close();\n } catch {\n /* ignore close errors so all watchers are attempted */\n }\n }\n watchers.clear();\n activeCallbacks.clear();\n subscriberCounts.clear();\n desiredState.clear();\n },\n };\n}\n\nexport type WatcherRegistry = ReturnType<typeof createWatcherRegistry>;\n"]}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Request, RequestHandler, Response } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* The one JSON-RPC error envelope every HTTP refusal goes out in — this
|
|
4
|
+
* module's pre-handler rejections and `transport.ts`'s. It lives here because
|
|
5
|
+
* `transport.ts` already imports this module and not the other way round; a
|
|
6
|
+
* second hand-built literal is how the two drift apart.
|
|
7
|
+
*
|
|
8
|
+
* `headers` carries what a particular refusal owes the client on top of the
|
|
9
|
+
* JSON content type — `WWW-Authenticate` on a 401, `Retry-After` on a 429.
|
|
10
|
+
* Goes out through `res.json` so every refusal keeps the `charset=utf-8` and
|
|
11
|
+
* `Content-Length` a hand-rolled `writeHead`/`end` pair drops.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sendJsonRpcError(res: Response, status: number, code: number, message: string, id?: string | number | null, headers?: Record<string, string>): void;
|
|
14
|
+
/**
|
|
15
|
+
* Pure HTTP auth and binding policy. Holds no state; all functions are
|
|
16
|
+
* directly testable without spinning up a server.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isLoopbackHttpHost(host: string): boolean;
|
|
19
|
+
export declare function isAllowedLocalhostOrigin(origin: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* True if `origin` (a raw `Origin` request header) is allowed given the
|
|
22
|
+
* env-derived `allowedHostnames` set (hostname-form, no scheme/port). Localhost
|
|
23
|
+
* origins are always accepted via {@link isAllowedLocalhostOrigin}; a remote
|
|
24
|
+
* origin is accepted iff its parsed hostname is in the set. Both the SDK app's
|
|
25
|
+
* `allowedOrigins` and this OPTIONS-handler check consume hostname-form, so a
|
|
26
|
+
* remote origin allowed via `FS_ALLOWED_ORIGINS` is reflected
|
|
27
|
+
* end-to-end in `Access-Control-Allow-Origin`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isOriginAllowed(origin: string, allowedHostnames: readonly string[]): boolean;
|
|
30
|
+
export declare function validateBearerAuthorization(apiKey: string, authHeader: unknown): authHeader is string;
|
|
31
|
+
/**
|
|
32
|
+
* Refuse to bind to a non-loopback host without an API key. Throws on
|
|
33
|
+
* policy violation; returns silently when allowed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertHttpBindingPolicy(host: string, apiKey: string | undefined): void;
|
|
36
|
+
/**
|
|
37
|
+
* The Host header values this bind accepts. `FS_ALLOWED_HOSTS` wins
|
|
38
|
+
* when set; otherwise a loopback bind takes the whole localhost hostname set (a
|
|
39
|
+
* client dialing http://localhost:<port> sends `Host: localhost`, which a bare
|
|
40
|
+
* ['127.0.0.1'] list would 403), a wildcard bind takes none, and a concrete
|
|
41
|
+
* non-loopback bind takes itself. An empty result means no Host validation is
|
|
42
|
+
* mounted — only reachable under FS_ALLOW_UNRESTRICTED_HOSTS=1.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveAllowedHosts(httpHost: string, allowedHostsEnv: string | undefined): string[];
|
|
45
|
+
/**
|
|
46
|
+
* Express's `trust proxy` setting from `FS_TRUST_PROXY`. A
|
|
47
|
+
* non-negative integer hop count parses to a number (Express counts hops from
|
|
48
|
+
* the socket); anything else (a subnet name/expression, or a negative/
|
|
49
|
+
* non-integer string) passes through unchanged for Express to interpret.
|
|
50
|
+
* `undefined` means the env var was unset or empty — leave Express's default
|
|
51
|
+
* (disabled) in place.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveTrustProxySetting(value: string | undefined): number | string | undefined;
|
|
54
|
+
/**
|
|
55
|
+
* Refuse to bind a wildcard host (`0.0.0.0` / `::`) without an explicit
|
|
56
|
+
* `FS_ALLOWED_HOSTS` list. Clients never send `Host: 0.0.0.0`, so
|
|
57
|
+
* defaulting the allowed-host set to the wildcard string would reject all real
|
|
58
|
+
* traffic. Operators who accept the risk can set
|
|
59
|
+
* `FS_ALLOW_UNRESTRICTED_HOSTS=1` to restore warn-and-bind.
|
|
60
|
+
* Loopback and concrete non-loopback hosts are unaffected.
|
|
61
|
+
*/
|
|
62
|
+
export declare function assertHttpHostPolicy(host: string, allowedHosts: readonly string[], allowUnrestricted: boolean): void;
|
|
63
|
+
/**
|
|
64
|
+
* This server is a resource server with no authorization server: `API_KEY` is a
|
|
65
|
+
* static secret the operator hands out of band, not an issued token. So the
|
|
66
|
+
* metadata document deliberately omits `authorization_servers` — RFC 9728 §2
|
|
67
|
+
* makes it optional, and its absence is the accurate statement that a token
|
|
68
|
+
* cannot be obtained from an endpoint. `mcpAuthMetadataRouter` from
|
|
69
|
+
* `@modelcontextprotocol/express` is the tool for the IdP-backed case: it
|
|
70
|
+
* requires RFC 8414 authorization-server metadata, and inventing an issuer with
|
|
71
|
+
* endpoints that answer nothing would send clients into a flow that cannot
|
|
72
|
+
* complete. Adopt it if this ever moves to a real IdP.
|
|
73
|
+
*
|
|
74
|
+
* What discovery buys here: a client hitting 401 learns the resource identifier
|
|
75
|
+
* and that the credential goes in the Authorization header, instead of a bare
|
|
76
|
+
* challenge plus a 404 on the well-known path.
|
|
77
|
+
*/
|
|
78
|
+
export declare function protectedResourceUrl(req: Request, hostValidated: boolean, configured: string | undefined): URL | null;
|
|
79
|
+
/**
|
|
80
|
+
* Express middleware: when `apiKey` is set, require a matching bearer token.
|
|
81
|
+
* No key set = open access (loopback dev mode). `apiKey` is captured once per
|
|
82
|
+
* app setup (passed in from startHttpServer) so the middleware and
|
|
83
|
+
* assertHttpBindingPolicy share one source of truth.
|
|
84
|
+
*
|
|
85
|
+
* `requireBearerAuth` + `verifyAccessToken` from `@modelcontextprotocol/express`
|
|
86
|
+
* is the tool for verifier-backed tokens; this middleware stays hand-rolled
|
|
87
|
+
* because the credential is a static operator key with no verifier, the 401 body
|
|
88
|
+
* must stay a JSON-RPC envelope, and the RFC 6750 bare-vs-`invalid_token`
|
|
89
|
+
* challenge split is implemented here. Adopt `requireBearerAuth` if this ever
|
|
90
|
+
* verifies issued tokens.
|
|
91
|
+
*/
|
|
92
|
+
export declare function bearerAuthMiddleware(apiKey: string | undefined, hostValidated: boolean, publicUrl?: string): RequestHandler;
|
|
93
|
+
/**
|
|
94
|
+
* Env-derived CORS origins (hostname-form, no scheme/port — matches the SDK
|
|
95
|
+
* app's `allowedOrigins` consumer). Defaults to the SDK's loopback hostname set
|
|
96
|
+
* so loopback browser clients keep working; operators set
|
|
97
|
+
* `FS_ALLOWED_ORIGINS` to allow remote clients on non-loopback
|
|
98
|
+
* binds. An empty value reads as unset, matching how parseAllowedHostsEnv
|
|
99
|
+
* treats an all-empty list. The same set is consulted by corsPreflightHandler
|
|
100
|
+
* so a remote origin is reflected end-to-end in Access-Control-Allow-Origin.
|
|
101
|
+
*/
|
|
102
|
+
export declare function computeAllowedOriginHostnames(originsEnv: string | undefined): string[];
|
|
103
|
+
/** Mounted ahead of the `/mcp` handlers so every response — not just the
|
|
104
|
+
* OPTIONS preflight — carries `Access-Control-Allow-Origin` for an allowed
|
|
105
|
+
* Origin. */
|
|
106
|
+
export declare function corsOriginMiddleware(allowedOriginHostnames: readonly string[]): RequestHandler;
|
|
107
|
+
/**
|
|
108
|
+
* OPTIONS preflight for `/mcp`. The allow-list carries the SEP-2243 standard
|
|
109
|
+
* headers (`mcp-protocol-version`, `mcp-method`, `mcp-name`): SDK clients send
|
|
110
|
+
* all three on every modern request POST and `createMcpHandler` requires them
|
|
111
|
+
* (400 / -32020), so omitting them here would fail every browser preflight —
|
|
112
|
+
* and the HTTP leg is modern-only, so there is no legacy path to fall back to.
|
|
113
|
+
* `mcp-session-id` is deliberately absent: it is 2025-era only and this
|
|
114
|
+
* endpoint rejects legacy traffic.
|
|
115
|
+
*/
|
|
116
|
+
export declare function corsPreflightHandler(allowedOriginHostnames: readonly string[]): RequestHandler;
|
|
117
|
+
/**
|
|
118
|
+
* Fixed-window per-client-IP rate limiter.
|
|
119
|
+
*/
|
|
120
|
+
export declare function createRateLimiter(max: number): RequestHandler;
|
|
121
|
+
//# sourceMappingURL=http-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-policy.d.ts","sourceRoot":"","sources":["../src/http-policy.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAgB,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAe/E;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,QAAQ,EACb,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,EAAE,GAAE,MAAM,GAAG,MAAM,GAAG,IAAW,EACjC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACnC,IAAI,CAEN;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQxD;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEhE;AAgBD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAI5F;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,OAAO,GAClB,UAAU,IAAI,MAAM,CAiBtB;AAMD;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAetF;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,GAAG,SAAS,GAClC,MAAM,EAAE,CAMV;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAI/F;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,iBAAiB,EAAE,OAAO,GACzB,IAAI,CAUN;AAID;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,aAAa,EAAE,OAAO,EACtB,UAAU,EAAE,MAAM,GAAG,SAAS,GAC7B,GAAG,GAAG,IAAI,CAcZ;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,aAAa,EAAE,OAAO,EACtB,SAAS,CAAC,EAAE,MAAM,GACjB,cAAc,CAgChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAKtF;AAyBD;;aAEa;AACb,wBAAgB,oBAAoB,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAK9F;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,sBAAsB,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAU9F;AAYD;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CA+B7D"}
|