@j0hanz/filesystem-mcp 1.19.1 → 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 -585
- 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 -473
- 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,261 @@
|
|
|
1
|
+
import { acceptedContent, createRequestStateCodec, inputRequired, } from '@modelcontextprotocol/server';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import * as z from 'zod/v4';
|
|
4
|
+
import { ErrorCode, FsError } from './errors.js';
|
|
5
|
+
import { Logger } from './observability.js';
|
|
6
|
+
/**
|
|
7
|
+
* HMAC key for the requestState codec. Read once from
|
|
8
|
+
* `FS_REQUEST_STATE_KEY` (UTF-8, must be >=32 bytes); a random
|
|
9
|
+
* 32-byte key is generated at boot when the env var is unset or too short. A
|
|
10
|
+
* per-process key is correct for stdio and the single-node HTTP leg (decision
|
|
11
|
+
* record 11). For a multi-instance HTTP fleet behind a load balancer, a random
|
|
12
|
+
* per-process key silently breaks any `input_required` round that lands on a
|
|
13
|
+
* different instance — `assertFleetRequestStateKey()` is the boot-time HTTP
|
|
14
|
+
* guard that refuses to start the HTTP leg in that state. It is NOT called at
|
|
15
|
+
* module load (the codec is constructed at module scope, before the stdio/HTTP
|
|
16
|
+
* decision in `main()`), so a stdio launch with `FS_API_KEY` exported still boots.
|
|
17
|
+
* A server restart invalidates in-flight tokens; the client re-requests, which
|
|
18
|
+
* is fail-closed and safe.
|
|
19
|
+
*/
|
|
20
|
+
function configuredRequestStateKey() {
|
|
21
|
+
const env = process.env['FS_REQUEST_STATE_KEY'];
|
|
22
|
+
if (env) {
|
|
23
|
+
const bytes = Buffer.from(env, 'utf8');
|
|
24
|
+
if (bytes.length >= 32)
|
|
25
|
+
return bytes;
|
|
26
|
+
Logger.warn(`FS_REQUEST_STATE_KEY is ${String(bytes.length)} bytes; 32 are required. Falling back to a random per-boot key — in-flight input_required rounds will not survive a restart.`);
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
// Built on first use, not at module load: `startHttpServer` gets to enforce the
|
|
31
|
+
// fleet key requirement before anything mints with a random per-boot fallback.
|
|
32
|
+
let codec;
|
|
33
|
+
function getRequestStateCodec() {
|
|
34
|
+
codec ??= createRequestStateCodec({
|
|
35
|
+
key: configuredRequestStateKey() ?? randomBytes(32),
|
|
36
|
+
});
|
|
37
|
+
return codec;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Boot-time HTTP guard: when the HTTP leg is active (`FS_API_KEY` set) and
|
|
41
|
+
* `FS_REQUEST_STATE_KEY` is missing or <32 bytes, refuse to start —
|
|
42
|
+
* a multi-instance fleet behind a load balancer would otherwise silently break
|
|
43
|
+
* every `input_required` round that lands on a different instance (each node
|
|
44
|
+
* mints tokens with its own random per-boot key). No-op outside explicit fleet
|
|
45
|
+
* mode. Called from `startHttpServer`, never at module load.
|
|
46
|
+
*/
|
|
47
|
+
export function assertFleetRequestStateKey(fleet) {
|
|
48
|
+
if (!fleet)
|
|
49
|
+
return;
|
|
50
|
+
if (!configuredRequestStateKey()) {
|
|
51
|
+
throw new Error('FS_REQUEST_STATE_KEY must be >=32 bytes in fleet deployment mode.');
|
|
52
|
+
}
|
|
53
|
+
getRequestStateCodec();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The codec: `mint` seals a `PendingState` into the opaque wire string a
|
|
57
|
+
* handler returns from `inputRequired({ requestState })`; `verify` drops into
|
|
58
|
+
* `ServerOptions.requestState.verify` and throws on tamper, expiry, or bind
|
|
59
|
+
* mismatch (the seam answers with the frozen `-32602`).
|
|
60
|
+
*/
|
|
61
|
+
export const requestStateCodec = {
|
|
62
|
+
mint: (payload, ctx) => getRequestStateCodec().mint(payload, ctx),
|
|
63
|
+
verify: (state, ctx) => getRequestStateCodec().verify(state, ctx),
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Build a boolean confirmation input for one pending item. The schema uses a
|
|
67
|
+
* fixed `confirm` boolean field; the input request is keyed by `key` so a
|
|
68
|
+
* batched call carries one request per item under a distinct key.
|
|
69
|
+
*/
|
|
70
|
+
export function confirmInput(key, message) {
|
|
71
|
+
return { key, message };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Build a single-select enum confirmation input. The form renders a `choice`
|
|
75
|
+
* field whose options are the titled `choices`; the caller reads the selection
|
|
76
|
+
* with `readAcceptedChoice`. A `defaultValue` preselects one option.
|
|
77
|
+
*/
|
|
78
|
+
export function choiceInput(key, message, choices, defaultValue) {
|
|
79
|
+
return { key, message, choices, ...(defaultValue !== undefined ? { defaultValue } : {}) };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build a multi-select enum confirmation input. Like `choiceInput` but the
|
|
83
|
+
* form renders `choice` as a string array (`MultiSelectEnumSchema` shape), so
|
|
84
|
+
* the client may accept a subset; the caller reads the accepted set with
|
|
85
|
+
* `readAcceptedMultiChoice`.
|
|
86
|
+
*/
|
|
87
|
+
export function multiSelectInput(key, message, choices) {
|
|
88
|
+
return {
|
|
89
|
+
key,
|
|
90
|
+
message,
|
|
91
|
+
choices,
|
|
92
|
+
multi: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build an `input_required` result carrying one boolean confirmation per pending
|
|
97
|
+
* item, plus the integrity-protected `requestState` sealing the operation kind
|
|
98
|
+
* and sorted target paths. `mint` is async (HMAC + base64url).
|
|
99
|
+
*/
|
|
100
|
+
export async function buildInputRequired(pending, inputs) {
|
|
101
|
+
const inputRequests = {};
|
|
102
|
+
for (const input of inputs) {
|
|
103
|
+
const requestedSchema = input.choices
|
|
104
|
+
? input.multi
|
|
105
|
+
? {
|
|
106
|
+
// Multi-select enum (matches MultiSelectEnumSchema): `choice` is a
|
|
107
|
+
// string array; the client may accept a subset of the offered dirs.
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {
|
|
110
|
+
choice: {
|
|
111
|
+
type: 'array',
|
|
112
|
+
items: {
|
|
113
|
+
anyOf: input.choices.map((c) => ({ const: c.value, title: c.title })),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ['choice'],
|
|
118
|
+
}
|
|
119
|
+
: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
properties: {
|
|
122
|
+
choice: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
oneOf: input.choices.map((c) => ({ const: c.value, title: c.title })),
|
|
125
|
+
...(input.defaultValue !== undefined ? { default: input.defaultValue } : {}),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ['choice'],
|
|
129
|
+
}
|
|
130
|
+
: {
|
|
131
|
+
type: 'object',
|
|
132
|
+
properties: { confirm: { type: 'boolean', title: 'Confirm' } },
|
|
133
|
+
required: ['confirm'],
|
|
134
|
+
};
|
|
135
|
+
inputRequests[input.key] = inputRequired.elicit({
|
|
136
|
+
message: input.message,
|
|
137
|
+
requestedSchema,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const requestState = await requestStateCodec.mint({
|
|
141
|
+
op: pending.op,
|
|
142
|
+
paths: [...pending.paths].sort(),
|
|
143
|
+
});
|
|
144
|
+
return inputRequired({ inputRequests, requestState });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* What to tell the model when the round-trip cannot happen. Each names the way
|
|
148
|
+
* forward that does not need a confirmation, or says plainly that there is
|
|
149
|
+
* none — a dead end the model can report is worth more than a retry loop.
|
|
150
|
+
*/
|
|
151
|
+
const NO_ELICITATION_HINT = {
|
|
152
|
+
delete: 'Deleting a non-empty directory needs a confirmation this client cannot show. ' +
|
|
153
|
+
'Delete the entries inside it individually, or connect a client that declares the elicitation capability.',
|
|
154
|
+
move: 'Overwriting an existing destination needs a confirmation this client cannot show. ' +
|
|
155
|
+
'Delete the destination first, or move to a path that does not exist yet.',
|
|
156
|
+
copy: 'Overwriting an existing destination needs a confirmation this client cannot show. ' +
|
|
157
|
+
'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',
|
|
158
|
+
grant: 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +
|
|
159
|
+
'Call list_roots and use a path under one of the roots it returns.',
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Fail early, and legibly, when the client cannot answer an embedded request.
|
|
163
|
+
* The SDK checks each `inputRequests` entry against the declared client
|
|
164
|
+
* capabilities and rejects the whole call with `-32021` before anything reaches
|
|
165
|
+
* the wire — a protocol error the model never sees. Throwing `FsError` here
|
|
166
|
+
* instead lands in the tool executor's catch and reaches the model as an
|
|
167
|
+
* `isError` result carrying the workaround.
|
|
168
|
+
*
|
|
169
|
+
* `undefined` capabilities mean the connection cannot report them, NOT that the
|
|
170
|
+
* client has none: proceed and let the SDK decide, exactly as before this check
|
|
171
|
+
* existed.
|
|
172
|
+
*/
|
|
173
|
+
function assertCanElicit(op, capabilities) {
|
|
174
|
+
if (capabilities === undefined)
|
|
175
|
+
return;
|
|
176
|
+
if (capabilities.elicitation !== undefined)
|
|
177
|
+
return;
|
|
178
|
+
throw new FsError(ErrorCode.INVALID_INPUT, `${op}: ${NO_ELICITATION_HINT[op]}`);
|
|
179
|
+
}
|
|
180
|
+
export async function pendingRoundTrip(opts) {
|
|
181
|
+
const state = opts.requestState?.();
|
|
182
|
+
// No verified state yet, OR the verified state belongs to a different
|
|
183
|
+
// flow (e.g. an access-grant round's state is still the retried request's
|
|
184
|
+
// requestState after the grant was applied, and this call is now the
|
|
185
|
+
// destructive-confirmation round for the SAME tool call) — either way,
|
|
186
|
+
// this op has not yet had its own round-trip, so mint a fresh
|
|
187
|
+
// input_required rather than treating a foreign-but-valid state as a
|
|
188
|
+
// tamper/mismatch error.
|
|
189
|
+
if (state?.op !== opts.op) {
|
|
190
|
+
assertCanElicit(opts.op, opts.clientCapabilities);
|
|
191
|
+
return buildInputRequired({ op: opts.op, paths: opts.pending }, opts.buildInputs(opts.pending));
|
|
192
|
+
}
|
|
193
|
+
// Retry for THIS op (R9): the verified state must bind the same pending set.
|
|
194
|
+
if (!pathsEqual(state.paths, opts.pending)) {
|
|
195
|
+
throw new FsError(ErrorCode.INVALID_INPUT, `${opts.op}: confirmation does not match the requested paths`);
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
// Response shapes handed to the SDK's schema-aware `acceptedContent` overload:
|
|
200
|
+
// it returns `undefined` for a missing key, a decline/cancel, a non-elicit
|
|
201
|
+
// response, AND a payload that fails validation — one call covers every refusal
|
|
202
|
+
// case the readers below used to hand-check.
|
|
203
|
+
const ConfirmContent = z.object({ confirm: z.boolean() });
|
|
204
|
+
const ChoiceContent = z.object({ choice: z.string() });
|
|
205
|
+
const MultiChoiceContent = z.object({ choice: z.array(z.string()) });
|
|
206
|
+
/**
|
|
207
|
+
* Read one pending item's boolean confirmation from a retried request's
|
|
208
|
+
* `inputResponses`. Returns `true` only when the client explicitly accepted AND
|
|
209
|
+
* the `confirm` field is `true`; every other outcome (decline, cancel, missing
|
|
210
|
+
* key, accept-without-confirm) returns `false`, which the caller reports as
|
|
211
|
+
* `CANCELLED` (R3 proceed, R4/R5 cancelled).
|
|
212
|
+
*/
|
|
213
|
+
export function readAcceptedConfirm(responses, key) {
|
|
214
|
+
return acceptedContent(responses, key, ConfirmContent)?.confirm === true;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Read one pending item's enum selection from a retried request's
|
|
218
|
+
* `inputResponses`. Returns the chosen `value` only when the client explicitly
|
|
219
|
+
* accepted AND the `choice` field is a string; every other outcome (decline,
|
|
220
|
+
* cancel, missing key, accept-without-choice) returns `undefined`, which the
|
|
221
|
+
* caller reports as `CANCELLED` — same contract as the boolean reader.
|
|
222
|
+
*
|
|
223
|
+
* The shape is SDK-validated, but the returned value is NOT checked for
|
|
224
|
+
* membership in the `choices` offered in the request schema. Callers must
|
|
225
|
+
* compare against the expected values (e.g. `choice === 'overwrite'`) before
|
|
226
|
+
* acting — never echo the string back into a path or trust it as an enum
|
|
227
|
+
* member.
|
|
228
|
+
*/
|
|
229
|
+
export function readAcceptedChoice(responses, key) {
|
|
230
|
+
return acceptedContent(responses, key, ChoiceContent)?.choice;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Read one pending item's multi-select enum selection from a retried request's
|
|
234
|
+
* `inputResponses`. Returns the accepted `value`s only when the client
|
|
235
|
+
* explicitly accepted AND `choice` is a string array of strings; every other
|
|
236
|
+
* outcome (decline, cancel, missing key, accept-without-choice, non-array
|
|
237
|
+
* `choice`, non-string elements) returns `undefined`, which the caller reports
|
|
238
|
+
* as `CANCELLED` — same contract as the single-select reader.
|
|
239
|
+
*
|
|
240
|
+
* As with `readAcceptedChoice`, the returned values are NOT checked for
|
|
241
|
+
* membership in the offered `choices`; callers must compare against the
|
|
242
|
+
* expected set before acting.
|
|
243
|
+
*/
|
|
244
|
+
export function readAcceptedMultiChoice(responses, key) {
|
|
245
|
+
return acceptedContent(responses, key, MultiChoiceContent)?.choice;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Order-independent equality of two sorted, de-duplicated path lists. Used to
|
|
249
|
+
* compare a recomputed pending set against the `requestState`-bound set (R9):
|
|
250
|
+
* both are sorted by construction, so a straight element-wise compare settles
|
|
251
|
+
* it without allocating.
|
|
252
|
+
*/
|
|
253
|
+
function pathsEqual(a, b) {
|
|
254
|
+
if (a.length !== b.length)
|
|
255
|
+
return false;
|
|
256
|
+
for (let i = 0; i < a.length; i++)
|
|
257
|
+
if (a[i] !== b[i])
|
|
258
|
+
return false;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
//# sourceMappingURL=input-required.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-required.js","sourceRoot":"","sources":["../../src/core/input-required.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared infrastructure for the SEP-2577 `input_required` multi-round-trip\n// destructive-confirmation flows (recursive delete, move overwrite, out-of-root\n// access grant). A handler returns `inputRequired(...)` instead of the deprecated\n// push-style server-to-client elicitation request; the client retries the same\n// `tools/call` carrying `inputResponses`, and the handler re-enters from the top\n// reading the verified `requestState` and the accepted responses.\n//\n// `requestState` round-trips through the client (attacker-controlled on\n// re-entry), so it is sealed with the SDK's HMAC-SHA256 codec. The payload binds\n// each confirmation to its operation kind and sorted target-path set (spec R9,\n// R10); the handler additionally rejects any retry whose decoded paths do not\n// match the retried request's parameters (the codec only proves the state was\n// not tampered — it cannot see the current args).\nimport type {\n ClientCapabilities,\n InputRequest,\n InputRequiredResult,\n RequestStateCodec,\n} from '@modelcontextprotocol/server';\nimport {\n acceptedContent,\n createRequestStateCodec,\n inputRequired,\n} from '@modelcontextprotocol/server';\n\nimport { randomBytes } from 'node:crypto';\n\nimport * as z from 'zod/v4';\n\nimport { ErrorCode, FsError } from './errors.js';\nimport { Logger } from './observability.js';\n\n/** The destructive operation a pending confirmation authorizes. */\ntype PendingOp = 'delete' | 'move' | 'copy' | 'grant';\n\n/**\n * Integrity-protected state minted into an `input_required` result and echoed\n * back by the client on retry. `paths` is the sorted canonical set of target\n * paths the confirmation covers; the handler compares it to the retried\n * request's parameters and rejects a mismatch (R9).\n */\nexport interface PendingState {\n readonly op: PendingOp;\n readonly paths: readonly string[];\n}\n\n/** One embedded form-mode confirmation, keyed within the call. */\nexport interface PendingInput {\n /** Server-assigned key, unique within the `tools/call`. */\n readonly key: string;\n /** Human-readable prompt for this item. */\n readonly message: string;\n /**\n * When set, the form offers a titled single-select enum (`choice` field)\n * instead of a boolean `confirm`. Each entry is a `{ value, title }` pair.\n */\n readonly choices?: readonly { value: string; title: string }[];\n /** Preselected enum value (only meaningful with single-select `choices`). */\n readonly defaultValue?: string;\n /**\n * When set with `choices`, the form offers a multi-select enum: `choice` is\n * a string array (`MultiSelectEnumSchema` shape) and the caller reads the\n * accepted set with `readAcceptedMultiChoice`. Single-select otherwise.\n */\n readonly multi?: boolean;\n}\n\n/**\n * HMAC key for the requestState codec. Read once from\n * `FS_REQUEST_STATE_KEY` (UTF-8, must be >=32 bytes); a random\n * 32-byte key is generated at boot when the env var is unset or too short. A\n * per-process key is correct for stdio and the single-node HTTP leg (decision\n * record 11). For a multi-instance HTTP fleet behind a load balancer, a random\n * per-process key silently breaks any `input_required` round that lands on a\n * different instance — `assertFleetRequestStateKey()` is the boot-time HTTP\n * guard that refuses to start the HTTP leg in that state. It is NOT called at\n * module load (the codec is constructed at module scope, before the stdio/HTTP\n * decision in `main()`), so a stdio launch with `FS_API_KEY` exported still boots.\n * A server restart invalidates in-flight tokens; the client re-requests, which\n * is fail-closed and safe.\n */\nfunction configuredRequestStateKey(): Uint8Array | undefined {\n const env = process.env['FS_REQUEST_STATE_KEY'];\n if (env) {\n const bytes = Buffer.from(env, 'utf8');\n if (bytes.length >= 32) return bytes;\n Logger.warn(\n `FS_REQUEST_STATE_KEY is ${String(bytes.length)} bytes; 32 are required. Falling back to a random per-boot key — in-flight input_required rounds will not survive a restart.`,\n );\n }\n return undefined;\n}\n\n// Built on first use, not at module load: `startHttpServer` gets to enforce the\n// fleet key requirement before anything mints with a random per-boot fallback.\nlet codec: RequestStateCodec<PendingState> | undefined;\n\nfunction getRequestStateCodec(): RequestStateCodec<PendingState> {\n codec ??= createRequestStateCodec<PendingState>({\n key: configuredRequestStateKey() ?? randomBytes(32),\n });\n return codec;\n}\n\n/**\n * Boot-time HTTP guard: when the HTTP leg is active (`FS_API_KEY` set) and\n * `FS_REQUEST_STATE_KEY` is missing or <32 bytes, refuse to start —\n * a multi-instance fleet behind a load balancer would otherwise silently break\n * every `input_required` round that lands on a different instance (each node\n * mints tokens with its own random per-boot key). No-op outside explicit fleet\n * mode. Called from `startHttpServer`, never at module load.\n */\nexport function assertFleetRequestStateKey(fleet: boolean): void {\n if (!fleet) return;\n if (!configuredRequestStateKey()) {\n throw new Error('FS_REQUEST_STATE_KEY must be >=32 bytes in fleet deployment mode.');\n }\n getRequestStateCodec();\n}\n\n/**\n * The codec: `mint` seals a `PendingState` into the opaque wire string a\n * handler returns from `inputRequired({ requestState })`; `verify` drops into\n * `ServerOptions.requestState.verify` and throws on tamper, expiry, or bind\n * mismatch (the seam answers with the frozen `-32602`).\n */\nexport const requestStateCodec: RequestStateCodec<PendingState> = {\n mint: (payload, ctx) => getRequestStateCodec().mint(payload, ctx),\n verify: (state, ctx) => getRequestStateCodec().verify(state, ctx),\n};\n\n/**\n * Build a boolean confirmation input for one pending item. The schema uses a\n * fixed `confirm` boolean field; the input request is keyed by `key` so a\n * batched call carries one request per item under a distinct key.\n */\nexport function confirmInput(key: string, message: string): PendingInput {\n return { key, message };\n}\n\n/**\n * Build a single-select enum confirmation input. The form renders a `choice`\n * field whose options are the titled `choices`; the caller reads the selection\n * with `readAcceptedChoice`. A `defaultValue` preselects one option.\n */\nexport function choiceInput(\n key: string,\n message: string,\n choices: readonly { value: string; title: string }[],\n defaultValue?: string,\n): PendingInput {\n return { key, message, choices, ...(defaultValue !== undefined ? { defaultValue } : {}) };\n}\n\n/**\n * Build a multi-select enum confirmation input. Like `choiceInput` but the\n * form renders `choice` as a string array (`MultiSelectEnumSchema` shape), so\n * the client may accept a subset; the caller reads the accepted set with\n * `readAcceptedMultiChoice`.\n */\nexport function multiSelectInput(\n key: string,\n message: string,\n choices: readonly { value: string; title: string }[],\n): PendingInput {\n return {\n key,\n message,\n choices,\n multi: true,\n };\n}\n\n/**\n * Build an `input_required` result carrying one boolean confirmation per pending\n * item, plus the integrity-protected `requestState` sealing the operation kind\n * and sorted target paths. `mint` is async (HMAC + base64url).\n */\nexport async function buildInputRequired(\n pending: PendingState,\n inputs: readonly PendingInput[],\n): Promise<InputRequiredResult> {\n const inputRequests: Record<string, InputRequest> = {};\n for (const input of inputs) {\n const requestedSchema = input.choices\n ? input.multi\n ? {\n // Multi-select enum (matches MultiSelectEnumSchema): `choice` is a\n // string array; the client may accept a subset of the offered dirs.\n type: 'object' as const,\n properties: {\n choice: {\n type: 'array' as const,\n items: {\n anyOf: input.choices.map((c) => ({ const: c.value, title: c.title })),\n },\n },\n },\n required: ['choice'],\n }\n : {\n type: 'object' as const,\n properties: {\n choice: {\n type: 'string' as const,\n oneOf: input.choices.map((c) => ({ const: c.value, title: c.title })),\n ...(input.defaultValue !== undefined ? { default: input.defaultValue } : {}),\n },\n },\n required: ['choice'],\n }\n : {\n type: 'object' as const,\n properties: { confirm: { type: 'boolean' as const, title: 'Confirm' } },\n required: ['confirm'],\n };\n inputRequests[input.key] = inputRequired.elicit({\n message: input.message,\n requestedSchema,\n });\n }\n const requestState = await requestStateCodec.mint({\n op: pending.op,\n paths: [...pending.paths].sort(),\n });\n return inputRequired({ inputRequests, requestState });\n}\n\n/**\n * The shared read-state → `buildInputRequired` → mismatch-throw flow used by\n * every destructive-confirmation handler (delete, move, grant). No verified\n * state, OR a verified state belonging to a different `op` (e.g. a chained\n * call's grant round already resolved and this is now that same call's own\n * delete/move confirmation round), mints a fresh `input_required` for this\n * op. A retry whose verified state matches this op but not the pending path\n * set throws `FsError(INVALID_INPUT)` (R9) — uniformly surfaced as an\n * `isError` tool result by the handler's catch. Returns `undefined` on a\n * matching same-op retry so the caller proceeds.\n *\n * One home for the R9 binding check means a future fix cannot miss two of three\n * sites. The caller supplies `buildInputs` so only the prompt text varies.\n */\nexport interface PendingRoundTripOpts {\n readonly op: PendingOp;\n readonly pending: readonly string[];\n readonly requestState: (() => PendingState | undefined) | undefined;\n /**\n * What the client declared it can do, or `undefined` when this connection\n * cannot say. Only a positively-absent `elicitation` short-circuits; see\n * {@link assertCanElicit}.\n */\n readonly clientCapabilities?: ClientCapabilities | undefined;\n readonly buildInputs: (pending: readonly string[]) => readonly PendingInput[];\n}\n\n/**\n * What to tell the model when the round-trip cannot happen. Each names the way\n * forward that does not need a confirmation, or says plainly that there is\n * none — a dead end the model can report is worth more than a retry loop.\n */\nconst NO_ELICITATION_HINT: Readonly<Record<PendingOp, string>> = {\n delete:\n 'Deleting a non-empty directory needs a confirmation this client cannot show. ' +\n 'Delete the entries inside it individually, or connect a client that declares the elicitation capability.',\n move:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Delete the destination first, or move to a path that does not exist yet.',\n copy:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',\n grant:\n 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +\n 'Call list_roots and use a path under one of the roots it returns.',\n};\n\n/**\n * Fail early, and legibly, when the client cannot answer an embedded request.\n * The SDK checks each `inputRequests` entry against the declared client\n * capabilities and rejects the whole call with `-32021` before anything reaches\n * the wire — a protocol error the model never sees. Throwing `FsError` here\n * instead lands in the tool executor's catch and reaches the model as an\n * `isError` result carrying the workaround.\n *\n * `undefined` capabilities mean the connection cannot report them, NOT that the\n * client has none: proceed and let the SDK decide, exactly as before this check\n * existed.\n */\nfunction assertCanElicit(op: PendingOp, capabilities: ClientCapabilities | undefined): void {\n if (capabilities === undefined) return;\n if (capabilities.elicitation !== undefined) return;\n throw new FsError(ErrorCode.INVALID_INPUT, `${op}: ${NO_ELICITATION_HINT[op]}`);\n}\n\nexport async function pendingRoundTrip(\n opts: PendingRoundTripOpts,\n): Promise<InputRequiredResult | undefined> {\n const state = opts.requestState?.();\n // No verified state yet, OR the verified state belongs to a different\n // flow (e.g. an access-grant round's state is still the retried request's\n // requestState after the grant was applied, and this call is now the\n // destructive-confirmation round for the SAME tool call) — either way,\n // this op has not yet had its own round-trip, so mint a fresh\n // input_required rather than treating a foreign-but-valid state as a\n // tamper/mismatch error.\n if (state?.op !== opts.op) {\n assertCanElicit(opts.op, opts.clientCapabilities);\n return buildInputRequired({ op: opts.op, paths: opts.pending }, opts.buildInputs(opts.pending));\n }\n // Retry for THIS op (R9): the verified state must bind the same pending set.\n if (!pathsEqual(state.paths, opts.pending)) {\n throw new FsError(\n ErrorCode.INVALID_INPUT,\n `${opts.op}: confirmation does not match the requested paths`,\n );\n }\n return undefined;\n}\n\n// Response shapes handed to the SDK's schema-aware `acceptedContent` overload:\n// it returns `undefined` for a missing key, a decline/cancel, a non-elicit\n// response, AND a payload that fails validation — one call covers every refusal\n// case the readers below used to hand-check.\nconst ConfirmContent = z.object({ confirm: z.boolean() });\nconst ChoiceContent = z.object({ choice: z.string() });\nconst MultiChoiceContent = z.object({ choice: z.array(z.string()) });\n\n/**\n * Read one pending item's boolean confirmation from a retried request's\n * `inputResponses`. Returns `true` only when the client explicitly accepted AND\n * the `confirm` field is `true`; every other outcome (decline, cancel, missing\n * key, accept-without-confirm) returns `false`, which the caller reports as\n * `CANCELLED` (R3 proceed, R4/R5 cancelled).\n */\nexport function readAcceptedConfirm(\n responses: Record<string, unknown> | undefined,\n key: string,\n): boolean {\n return acceptedContent(responses, key, ConfirmContent)?.confirm === true;\n}\n\n/**\n * Read one pending item's enum selection from a retried request's\n * `inputResponses`. Returns the chosen `value` only when the client explicitly\n * accepted AND the `choice` field is a string; every other outcome (decline,\n * cancel, missing key, accept-without-choice) returns `undefined`, which the\n * caller reports as `CANCELLED` — same contract as the boolean reader.\n *\n * The shape is SDK-validated, but the returned value is NOT checked for\n * membership in the `choices` offered in the request schema. Callers must\n * compare against the expected values (e.g. `choice === 'overwrite'`) before\n * acting — never echo the string back into a path or trust it as an enum\n * member.\n */\nexport function readAcceptedChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n return acceptedContent(responses, key, ChoiceContent)?.choice;\n}\n\n/**\n * Read one pending item's multi-select enum selection from a retried request's\n * `inputResponses`. Returns the accepted `value`s only when the client\n * explicitly accepted AND `choice` is a string array of strings; every other\n * outcome (decline, cancel, missing key, accept-without-choice, non-array\n * `choice`, non-string elements) returns `undefined`, which the caller reports\n * as `CANCELLED` — same contract as the single-select reader.\n *\n * As with `readAcceptedChoice`, the returned values are NOT checked for\n * membership in the offered `choices`; callers must compare against the\n * expected set before acting.\n */\nexport function readAcceptedMultiChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string[] | undefined {\n return acceptedContent(responses, key, MultiChoiceContent)?.choice;\n}\n\n/**\n * Order-independent equality of two sorted, de-duplicated path lists. Used to\n * compare a recomputed pending set against the `requestState`-bound set (R9):\n * both are sorted by construction, so a straight element-wise compare settles\n * it without allocating.\n */\nfunction pathsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
export declare const MIME_KINDS: readonly ["text", "binary", "image", "audio", "pdf"];
|
|
3
|
+
type MimeKind = (typeof MIME_KINDS)[number];
|
|
4
|
+
export interface MimeInfo {
|
|
5
|
+
mimeType: string;
|
|
6
|
+
kind: MimeKind;
|
|
7
|
+
}
|
|
8
|
+
export declare const MIME_SAMPLE_SIZE = 512;
|
|
9
|
+
export declare function isKnownBinaryExtension(filePath: string): boolean;
|
|
10
|
+
/** Single binary-vs-text verdict, shared by `detectMimeType` and the read path. */
|
|
11
|
+
export declare function isBinarySample(slice: Buffer): boolean;
|
|
12
|
+
export declare function detectMimeType(path: string, sample?: Buffer): MimeInfo;
|
|
13
|
+
export declare function detectMimeFromContent(path: string, content: string | Buffer): MimeInfo;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=mime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../../src/core/mime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAKrC,eAAO,MAAM,UAAU,sDAAuD,CAAC;AAC/E,KAAK,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;CAChB;AA0GD,eAAO,MAAM,gBAAgB,MAAM,CAAC;AA0CpC,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGhE;AAuBD,mFAAmF;AACnF,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAKrD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,CAmBtE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,CAOtF"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
4
|
+
export const MIME_KINDS = ['text', 'binary', 'image', 'audio', 'pdf'];
|
|
5
|
+
// ─── Extension Map ──────────────────────────────────────────────────────────
|
|
6
|
+
// Maps file extensions to MIME types (80+ common extensions)
|
|
7
|
+
const EXT_MAP = {
|
|
8
|
+
// Text: Web & Markup
|
|
9
|
+
html: { mimeType: 'text/html', kind: 'text' },
|
|
10
|
+
htm: { mimeType: 'text/html', kind: 'text' },
|
|
11
|
+
xml: { mimeType: 'text/xml', kind: 'text' },
|
|
12
|
+
css: { mimeType: 'text/css', kind: 'text' },
|
|
13
|
+
svg: { mimeType: 'image/svg+xml', kind: 'image' },
|
|
14
|
+
md: { mimeType: 'text/markdown', kind: 'text' },
|
|
15
|
+
markdown: { mimeType: 'text/markdown', kind: 'text' },
|
|
16
|
+
mdown: { mimeType: 'text/markdown', kind: 'text' },
|
|
17
|
+
// Text: Programming Languages
|
|
18
|
+
js: { mimeType: 'text/javascript', kind: 'text' },
|
|
19
|
+
mjs: { mimeType: 'text/javascript', kind: 'text' },
|
|
20
|
+
jsx: { mimeType: 'text/jsx', kind: 'text' },
|
|
21
|
+
ts: { mimeType: 'text/typescript', kind: 'text' },
|
|
22
|
+
tsx: { mimeType: 'text/tsx', kind: 'text' },
|
|
23
|
+
py: { mimeType: 'text/x-python', kind: 'text' },
|
|
24
|
+
java: { mimeType: 'text/x-java', kind: 'text' },
|
|
25
|
+
c: { mimeType: 'text/x-c', kind: 'text' },
|
|
26
|
+
cpp: { mimeType: 'text/x-cpp', kind: 'text' },
|
|
27
|
+
cc: { mimeType: 'text/x-cpp', kind: 'text' },
|
|
28
|
+
cxx: { mimeType: 'text/x-cpp', kind: 'text' },
|
|
29
|
+
h: { mimeType: 'text/x-c', kind: 'text' },
|
|
30
|
+
hpp: { mimeType: 'text/x-cpp', kind: 'text' },
|
|
31
|
+
go: { mimeType: 'text/x-go', kind: 'text' },
|
|
32
|
+
rs: { mimeType: 'text/x-rust', kind: 'text' },
|
|
33
|
+
rb: { mimeType: 'text/x-ruby', kind: 'text' },
|
|
34
|
+
php: { mimeType: 'text/x-php', kind: 'text' },
|
|
35
|
+
sh: { mimeType: 'text/x-shellscript', kind: 'text' },
|
|
36
|
+
bash: { mimeType: 'text/x-shellscript', kind: 'text' },
|
|
37
|
+
zsh: { mimeType: 'text/x-shellscript', kind: 'text' },
|
|
38
|
+
ps1: { mimeType: 'text/x-powershell', kind: 'text' },
|
|
39
|
+
sql: { mimeType: 'text/x-sql', kind: 'text' },
|
|
40
|
+
// Text: Data formats
|
|
41
|
+
json: { mimeType: 'application/json', kind: 'text' },
|
|
42
|
+
jsonc: { mimeType: 'application/json', kind: 'text' },
|
|
43
|
+
ndjson: { mimeType: 'application/x-ndjson', kind: 'text' },
|
|
44
|
+
yaml: { mimeType: 'text/yaml', kind: 'text' },
|
|
45
|
+
yml: { mimeType: 'text/yaml', kind: 'text' },
|
|
46
|
+
toml: { mimeType: 'text/toml', kind: 'text' },
|
|
47
|
+
ini: { mimeType: 'text/plain', kind: 'text' },
|
|
48
|
+
cfg: { mimeType: 'text/plain', kind: 'text' },
|
|
49
|
+
conf: { mimeType: 'text/plain', kind: 'text' },
|
|
50
|
+
csv: { mimeType: 'text/csv', kind: 'text' },
|
|
51
|
+
// Text: Diff & Patches
|
|
52
|
+
diff: { mimeType: 'text/x-diff', kind: 'text' },
|
|
53
|
+
patch: { mimeType: 'text/x-diff', kind: 'text' },
|
|
54
|
+
// Text: Documentation
|
|
55
|
+
txt: { mimeType: 'text/plain', kind: 'text' },
|
|
56
|
+
text: { mimeType: 'text/plain', kind: 'text' },
|
|
57
|
+
log: { mimeType: 'text/plain', kind: 'text' },
|
|
58
|
+
rst: { mimeType: 'text/x-rst', kind: 'text' },
|
|
59
|
+
// Image formats
|
|
60
|
+
png: { mimeType: 'image/png', kind: 'image' },
|
|
61
|
+
jpg: { mimeType: 'image/jpeg', kind: 'image' },
|
|
62
|
+
jpeg: { mimeType: 'image/jpeg', kind: 'image' },
|
|
63
|
+
gif: { mimeType: 'image/gif', kind: 'image' },
|
|
64
|
+
webp: { mimeType: 'image/webp', kind: 'image' },
|
|
65
|
+
ico: { mimeType: 'image/x-icon', kind: 'image' },
|
|
66
|
+
bmp: { mimeType: 'image/bmp', kind: 'image' },
|
|
67
|
+
tiff: { mimeType: 'image/tiff', kind: 'image' },
|
|
68
|
+
tif: { mimeType: 'image/tiff', kind: 'image' },
|
|
69
|
+
// Audio formats
|
|
70
|
+
mp3: { mimeType: 'audio/mpeg', kind: 'audio' },
|
|
71
|
+
wav: { mimeType: 'audio/wav', kind: 'audio' },
|
|
72
|
+
flac: { mimeType: 'audio/flac', kind: 'audio' },
|
|
73
|
+
aac: { mimeType: 'audio/aac', kind: 'audio' },
|
|
74
|
+
ogg: { mimeType: 'audio/ogg', kind: 'audio' },
|
|
75
|
+
m4a: { mimeType: 'audio/mp4', kind: 'audio' },
|
|
76
|
+
// PDF
|
|
77
|
+
pdf: { mimeType: 'application/pdf', kind: 'pdf' },
|
|
78
|
+
// Archives
|
|
79
|
+
zip: { mimeType: 'application/zip', kind: 'binary' },
|
|
80
|
+
tar: { mimeType: 'application/x-tar', kind: 'binary' },
|
|
81
|
+
gz: { mimeType: 'application/gzip', kind: 'binary' },
|
|
82
|
+
gzip: { mimeType: 'application/gzip', kind: 'binary' },
|
|
83
|
+
'7z': { mimeType: 'application/x-7z-compressed', kind: 'binary' },
|
|
84
|
+
rar: { mimeType: 'application/x-rar-compressed', kind: 'binary' },
|
|
85
|
+
bz2: { mimeType: 'application/x-bzip2', kind: 'binary' },
|
|
86
|
+
xz: { mimeType: 'application/x-xz', kind: 'binary' },
|
|
87
|
+
// Other binary formats
|
|
88
|
+
wasm: { mimeType: 'application/wasm', kind: 'binary' },
|
|
89
|
+
so: { mimeType: 'application/octet-stream', kind: 'binary' },
|
|
90
|
+
dylib: { mimeType: 'application/octet-stream', kind: 'binary' },
|
|
91
|
+
dll: { mimeType: 'application/octet-stream', kind: 'binary' },
|
|
92
|
+
exe: { mimeType: 'application/octet-stream', kind: 'binary' },
|
|
93
|
+
msi: { mimeType: 'application/octet-stream', kind: 'binary' },
|
|
94
|
+
dmg: { mimeType: 'application/octet-stream', kind: 'binary' },
|
|
95
|
+
};
|
|
96
|
+
// ─── Helper Functions ───────────────────────────────────────────────────────
|
|
97
|
+
export const MIME_SAMPLE_SIZE = 512;
|
|
98
|
+
// Every non-text kind is binary for read purposes, except SVG — an image kind
|
|
99
|
+
// whose bytes are text, and which the read path must not fast-path as binary.
|
|
100
|
+
// EXT_MAP covers the extensions it knows (image/audio/pdf/binary kinds all
|
|
101
|
+
// become binary here, image-kind SVG excepted); EXTRA_BINARY_EXTENSIONS lists
|
|
102
|
+
// the rest that EXT_MAP does not carry but the read path must still treat as
|
|
103
|
+
// binary. The two tables had drifted; this derivation resolves the drift toward
|
|
104
|
+
// binary (only changes whether a file skips its content probe).
|
|
105
|
+
const EXTRA_BINARY_EXTENSIONS = [
|
|
106
|
+
'mov',
|
|
107
|
+
'avi',
|
|
108
|
+
'mkv',
|
|
109
|
+
'webm',
|
|
110
|
+
'ttf',
|
|
111
|
+
'otf',
|
|
112
|
+
'woff',
|
|
113
|
+
'woff2',
|
|
114
|
+
'doc',
|
|
115
|
+
'docx',
|
|
116
|
+
'xls',
|
|
117
|
+
'xlsx',
|
|
118
|
+
'ppt',
|
|
119
|
+
'pptx',
|
|
120
|
+
'sqlite',
|
|
121
|
+
'db',
|
|
122
|
+
'bin',
|
|
123
|
+
'dat',
|
|
124
|
+
'mp4',
|
|
125
|
+
];
|
|
126
|
+
const KNOWN_BINARY_EXTENSIONS = new Set([
|
|
127
|
+
...Object.entries(EXT_MAP)
|
|
128
|
+
.filter(([, v]) => v.kind !== 'text')
|
|
129
|
+
.map(([k]) => k),
|
|
130
|
+
...EXTRA_BINARY_EXTENSIONS,
|
|
131
|
+
]
|
|
132
|
+
.filter((ext) => ext !== 'svg')
|
|
133
|
+
.map((ext) => `.${ext}`));
|
|
134
|
+
export function isKnownBinaryExtension(filePath) {
|
|
135
|
+
const ext = extname(filePath).toLowerCase();
|
|
136
|
+
return KNOWN_BINARY_EXTENSIONS.has(ext);
|
|
137
|
+
}
|
|
138
|
+
function hasUtf16Bom(slice) {
|
|
139
|
+
return (slice.length >= 2 &&
|
|
140
|
+
((slice[0] === 0xff && slice[1] === 0xfe) || (slice[0] === 0xfe && slice[1] === 0xff)));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* True when `slice` is valid UTF-8 *as a prefix*: `stream: true` parks a
|
|
144
|
+
* multi-byte sequence cut by the sample boundary in the decoder's buffer
|
|
145
|
+
* instead of reporting it, while `fatal: true` still throws on invalid bytes.
|
|
146
|
+
*/
|
|
147
|
+
function isUtf8Prefix(slice) {
|
|
148
|
+
try {
|
|
149
|
+
new TextDecoder('utf-8', { fatal: true }).decode(slice, { stream: true });
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/** Single binary-vs-text verdict, shared by `detectMimeType` and the read path. */
|
|
157
|
+
export function isBinarySample(slice) {
|
|
158
|
+
if (slice.length === 0)
|
|
159
|
+
return false;
|
|
160
|
+
if (hasUtf16Bom(slice))
|
|
161
|
+
return false;
|
|
162
|
+
if (slice.includes(0))
|
|
163
|
+
return true;
|
|
164
|
+
return !isUtf8Prefix(slice);
|
|
165
|
+
}
|
|
166
|
+
export function detectMimeType(path, sample) {
|
|
167
|
+
const lastDot = path.lastIndexOf('.');
|
|
168
|
+
const ext = lastDot > -1 ? path.slice(lastDot + 1).toLowerCase() : '';
|
|
169
|
+
if (ext && Object.hasOwn(EXT_MAP, ext)) {
|
|
170
|
+
const entry = EXT_MAP[ext];
|
|
171
|
+
if (entry !== undefined) {
|
|
172
|
+
return entry;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// No known extension: fall back to a binary/text probe of the content.
|
|
176
|
+
if (sample) {
|
|
177
|
+
return isBinarySample(sample.subarray(0, MIME_SAMPLE_SIZE))
|
|
178
|
+
? { mimeType: 'application/octet-stream', kind: 'binary' }
|
|
179
|
+
: { mimeType: 'text/plain', kind: 'text' };
|
|
180
|
+
}
|
|
181
|
+
return { mimeType: 'application/octet-stream', kind: 'binary' };
|
|
182
|
+
}
|
|
183
|
+
export function detectMimeFromContent(path, content) {
|
|
184
|
+
return detectMimeType(path, Buffer.isBuffer(content)
|
|
185
|
+
? content.subarray(0, MIME_SAMPLE_SIZE)
|
|
186
|
+
: Buffer.from(content.slice(0, MIME_SAMPLE_SIZE)));
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=mime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mime.js","sourceRoot":"","sources":["../../src/core/mime.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'node:buffer';\nimport { extname } from 'node:path';\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport const MIME_KINDS = ['text', 'binary', 'image', 'audio', 'pdf'] as const;\ntype MimeKind = (typeof MIME_KINDS)[number];\n\nexport interface MimeInfo {\n mimeType: string;\n kind: MimeKind;\n}\n\n// ─── Extension Map ──────────────────────────────────────────────────────────\n// Maps file extensions to MIME types (80+ common extensions)\n\nconst EXT_MAP: Record<string, { mimeType: string; kind: MimeKind }> = {\n // Text: Web & Markup\n html: { mimeType: 'text/html', kind: 'text' },\n htm: { mimeType: 'text/html', kind: 'text' },\n xml: { mimeType: 'text/xml', kind: 'text' },\n css: { mimeType: 'text/css', kind: 'text' },\n svg: { mimeType: 'image/svg+xml', kind: 'image' },\n md: { mimeType: 'text/markdown', kind: 'text' },\n markdown: { mimeType: 'text/markdown', kind: 'text' },\n mdown: { mimeType: 'text/markdown', kind: 'text' },\n\n // Text: Programming Languages\n js: { mimeType: 'text/javascript', kind: 'text' },\n mjs: { mimeType: 'text/javascript', kind: 'text' },\n jsx: { mimeType: 'text/jsx', kind: 'text' },\n ts: { mimeType: 'text/typescript', kind: 'text' },\n tsx: { mimeType: 'text/tsx', kind: 'text' },\n py: { mimeType: 'text/x-python', kind: 'text' },\n java: { mimeType: 'text/x-java', kind: 'text' },\n c: { mimeType: 'text/x-c', kind: 'text' },\n cpp: { mimeType: 'text/x-cpp', kind: 'text' },\n cc: { mimeType: 'text/x-cpp', kind: 'text' },\n cxx: { mimeType: 'text/x-cpp', kind: 'text' },\n h: { mimeType: 'text/x-c', kind: 'text' },\n hpp: { mimeType: 'text/x-cpp', kind: 'text' },\n go: { mimeType: 'text/x-go', kind: 'text' },\n rs: { mimeType: 'text/x-rust', kind: 'text' },\n rb: { mimeType: 'text/x-ruby', kind: 'text' },\n php: { mimeType: 'text/x-php', kind: 'text' },\n sh: { mimeType: 'text/x-shellscript', kind: 'text' },\n bash: { mimeType: 'text/x-shellscript', kind: 'text' },\n zsh: { mimeType: 'text/x-shellscript', kind: 'text' },\n ps1: { mimeType: 'text/x-powershell', kind: 'text' },\n sql: { mimeType: 'text/x-sql', kind: 'text' },\n\n // Text: Data formats\n json: { mimeType: 'application/json', kind: 'text' },\n jsonc: { mimeType: 'application/json', kind: 'text' },\n ndjson: { mimeType: 'application/x-ndjson', kind: 'text' },\n yaml: { mimeType: 'text/yaml', kind: 'text' },\n yml: { mimeType: 'text/yaml', kind: 'text' },\n toml: { mimeType: 'text/toml', kind: 'text' },\n ini: { mimeType: 'text/plain', kind: 'text' },\n cfg: { mimeType: 'text/plain', kind: 'text' },\n conf: { mimeType: 'text/plain', kind: 'text' },\n csv: { mimeType: 'text/csv', kind: 'text' },\n\n // Text: Diff & Patches\n diff: { mimeType: 'text/x-diff', kind: 'text' },\n patch: { mimeType: 'text/x-diff', kind: 'text' },\n\n // Text: Documentation\n txt: { mimeType: 'text/plain', kind: 'text' },\n text: { mimeType: 'text/plain', kind: 'text' },\n log: { mimeType: 'text/plain', kind: 'text' },\n rst: { mimeType: 'text/x-rst', kind: 'text' },\n\n // Image formats\n png: { mimeType: 'image/png', kind: 'image' },\n jpg: { mimeType: 'image/jpeg', kind: 'image' },\n jpeg: { mimeType: 'image/jpeg', kind: 'image' },\n gif: { mimeType: 'image/gif', kind: 'image' },\n webp: { mimeType: 'image/webp', kind: 'image' },\n ico: { mimeType: 'image/x-icon', kind: 'image' },\n bmp: { mimeType: 'image/bmp', kind: 'image' },\n tiff: { mimeType: 'image/tiff', kind: 'image' },\n tif: { mimeType: 'image/tiff', kind: 'image' },\n\n // Audio formats\n mp3: { mimeType: 'audio/mpeg', kind: 'audio' },\n wav: { mimeType: 'audio/wav', kind: 'audio' },\n flac: { mimeType: 'audio/flac', kind: 'audio' },\n aac: { mimeType: 'audio/aac', kind: 'audio' },\n ogg: { mimeType: 'audio/ogg', kind: 'audio' },\n m4a: { mimeType: 'audio/mp4', kind: 'audio' },\n\n // PDF\n pdf: { mimeType: 'application/pdf', kind: 'pdf' },\n\n // Archives\n zip: { mimeType: 'application/zip', kind: 'binary' },\n tar: { mimeType: 'application/x-tar', kind: 'binary' },\n gz: { mimeType: 'application/gzip', kind: 'binary' },\n gzip: { mimeType: 'application/gzip', kind: 'binary' },\n '7z': { mimeType: 'application/x-7z-compressed', kind: 'binary' },\n rar: { mimeType: 'application/x-rar-compressed', kind: 'binary' },\n bz2: { mimeType: 'application/x-bzip2', kind: 'binary' },\n xz: { mimeType: 'application/x-xz', kind: 'binary' },\n\n // Other binary formats\n wasm: { mimeType: 'application/wasm', kind: 'binary' },\n so: { mimeType: 'application/octet-stream', kind: 'binary' },\n dylib: { mimeType: 'application/octet-stream', kind: 'binary' },\n dll: { mimeType: 'application/octet-stream', kind: 'binary' },\n exe: { mimeType: 'application/octet-stream', kind: 'binary' },\n msi: { mimeType: 'application/octet-stream', kind: 'binary' },\n dmg: { mimeType: 'application/octet-stream', kind: 'binary' },\n};\n\n// ─── Helper Functions ───────────────────────────────────────────────────────\n\nexport const MIME_SAMPLE_SIZE = 512;\n\n// Every non-text kind is binary for read purposes, except SVG — an image kind\n// whose bytes are text, and which the read path must not fast-path as binary.\n// EXT_MAP covers the extensions it knows (image/audio/pdf/binary kinds all\n// become binary here, image-kind SVG excepted); EXTRA_BINARY_EXTENSIONS lists\n// the rest that EXT_MAP does not carry but the read path must still treat as\n// binary. The two tables had drifted; this derivation resolves the drift toward\n// binary (only changes whether a file skips its content probe).\nconst EXTRA_BINARY_EXTENSIONS = [\n 'mov',\n 'avi',\n 'mkv',\n 'webm',\n 'ttf',\n 'otf',\n 'woff',\n 'woff2',\n 'doc',\n 'docx',\n 'xls',\n 'xlsx',\n 'ppt',\n 'pptx',\n 'sqlite',\n 'db',\n 'bin',\n 'dat',\n 'mp4',\n];\n\nconst KNOWN_BINARY_EXTENSIONS: ReadonlySet<string> = new Set(\n [\n ...Object.entries(EXT_MAP)\n .filter(([, v]) => v.kind !== 'text')\n .map(([k]) => k),\n ...EXTRA_BINARY_EXTENSIONS,\n ]\n .filter((ext) => ext !== 'svg')\n .map((ext) => `.${ext}`),\n);\n\nexport function isKnownBinaryExtension(filePath: string): boolean {\n const ext = extname(filePath).toLowerCase();\n return KNOWN_BINARY_EXTENSIONS.has(ext);\n}\n\nfunction hasUtf16Bom(slice: Buffer): boolean {\n return (\n slice.length >= 2 &&\n ((slice[0] === 0xff && slice[1] === 0xfe) || (slice[0] === 0xfe && slice[1] === 0xff))\n );\n}\n\n/**\n * True when `slice` is valid UTF-8 *as a prefix*: `stream: true` parks a\n * multi-byte sequence cut by the sample boundary in the decoder's buffer\n * instead of reporting it, while `fatal: true` still throws on invalid bytes.\n */\nfunction isUtf8Prefix(slice: Buffer): boolean {\n try {\n new TextDecoder('utf-8', { fatal: true }).decode(slice, { stream: true });\n return true;\n } catch {\n return false;\n }\n}\n\n/** Single binary-vs-text verdict, shared by `detectMimeType` and the read path. */\nexport function isBinarySample(slice: Buffer): boolean {\n if (slice.length === 0) return false;\n if (hasUtf16Bom(slice)) return false;\n if (slice.includes(0)) return true;\n return !isUtf8Prefix(slice);\n}\n\nexport function detectMimeType(path: string, sample?: Buffer): MimeInfo {\n const lastDot = path.lastIndexOf('.');\n const ext = lastDot > -1 ? path.slice(lastDot + 1).toLowerCase() : '';\n\n if (ext && Object.hasOwn(EXT_MAP, ext)) {\n const entry = EXT_MAP[ext];\n if (entry !== undefined) {\n return entry;\n }\n }\n\n // No known extension: fall back to a binary/text probe of the content.\n if (sample) {\n return isBinarySample(sample.subarray(0, MIME_SAMPLE_SIZE))\n ? { mimeType: 'application/octet-stream', kind: 'binary' }\n : { mimeType: 'text/plain', kind: 'text' };\n }\n\n return { mimeType: 'application/octet-stream', kind: 'binary' };\n}\n\nexport function detectMimeFromContent(path: string, content: string | Buffer): MimeInfo {\n return detectMimeType(\n path,\n Buffer.isBuffer(content)\n ? content.subarray(0, MIME_SAMPLE_SIZE)\n : Buffer.from(content.slice(0, MIME_SAMPLE_SIZE)),\n );\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
|
|
2
|
+
export declare const Logger: {
|
|
3
|
+
emit: (level: LoggingLevel, message: string) => void;
|
|
4
|
+
info: (message: string, ...args: unknown[]) => void;
|
|
5
|
+
warn: (message: string, ...args: unknown[]) => void;
|
|
6
|
+
error: (message: string, ...args: unknown[]) => void;
|
|
7
|
+
debug: (message: string, ...args: unknown[]) => void;
|
|
8
|
+
};
|
|
9
|
+
export declare function logRuntimeFailure(id: string, scope: string, method: string, error: unknown): void;
|
|
10
|
+
//# sourceMappingURL=observability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GACtB,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;AA6DzF,eAAO,MAAM,MAAM;kBACH,YAAY,WAAW,MAAM;oBAG3B,MAAM,WAAW,OAAO,EAAE;oBAG1B,MAAM,WAAW,OAAO,EAAE;qBAGzB,MAAM,WAAW,OAAO,EAAE;qBAG1B,MAAM,WAAW,OAAO,EAAE;CAG5C,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAEjG"}
|