@sstolz/kdrive-mcp-server 0.0.0-stage → 0.6.0-beta.1
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/LICENSE +21 -0
- package/NOTICE +20 -0
- package/README.md +301 -2
- package/dist/audit/sink.d.ts +102 -0
- package/dist/audit/sink.js +225 -0
- package/dist/audit/sink.js.map +1 -0
- package/dist/auth/access-token.d.ts +63 -0
- package/dist/auth/access-token.js +199 -0
- package/dist/auth/access-token.js.map +1 -0
- package/dist/auth/proxy/index.d.ts +6 -0
- package/dist/auth/proxy/index.js +6 -0
- package/dist/auth/proxy/index.js.map +1 -0
- package/dist/auth/proxy/keys.d.ts +13 -0
- package/dist/auth/proxy/keys.js +42 -0
- package/dist/auth/proxy/keys.js.map +1 -0
- package/dist/auth/proxy/pages.d.ts +28 -0
- package/dist/auth/proxy/pages.js +58 -0
- package/dist/auth/proxy/pages.js.map +1 -0
- package/dist/auth/proxy/proxy.d.ts +63 -0
- package/dist/auth/proxy/proxy.js +1073 -0
- package/dist/auth/proxy/proxy.js.map +1 -0
- package/dist/auth/proxy/redirect.d.ts +2 -0
- package/dist/auth/proxy/redirect.js +34 -0
- package/dist/auth/proxy/redirect.js.map +1 -0
- package/dist/auth/proxy/store.d.ts +163 -0
- package/dist/auth/proxy/store.js +629 -0
- package/dist/auth/proxy/store.js.map +1 -0
- package/dist/auth/proxy/upstream.d.ts +23 -0
- package/dist/auth/proxy/upstream.js +85 -0
- package/dist/auth/proxy/upstream.js.map +1 -0
- package/dist/auth/verdict.d.ts +56 -0
- package/dist/auth/verdict.js +91 -0
- package/dist/auth/verdict.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-files.d.ts +5 -0
- package/dist/config-files.js +66 -0
- package/dist/config-files.js.map +1 -0
- package/dist/config-schema.d.ts +84 -0
- package/dist/config-schema.js +155 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config-sources.d.ts +10 -0
- package/dist/config-sources.js +202 -0
- package/dist/config-sources.js.map +1 -0
- package/dist/config-text.d.ts +6 -0
- package/dist/config-text.js +26 -0
- package/dist/config-text.js.map +1 -0
- package/dist/config-values.d.ts +5 -0
- package/dist/config-values.js +391 -0
- package/dist/config-values.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.js +186 -0
- package/dist/config.js.map +1 -0
- package/dist/content.d.ts +60 -0
- package/dist/content.js +280 -0
- package/dist/content.js.map +1 -0
- package/dist/extraction-image.d.ts +27 -0
- package/dist/extraction-image.js +204 -0
- package/dist/extraction-image.js.map +1 -0
- package/dist/extraction-owner.d.ts +4 -0
- package/dist/extraction-owner.js +22 -0
- package/dist/extraction-owner.js.map +1 -0
- package/dist/extraction-pdf-profile.d.ts +7 -0
- package/dist/extraction-pdf-profile.js +537 -0
- package/dist/extraction-pdf-profile.js.map +1 -0
- package/dist/extraction-pdf.d.ts +8 -0
- package/dist/extraction-pdf.js +89 -0
- package/dist/extraction-pdf.js.map +1 -0
- package/dist/extraction-text.d.ts +7 -0
- package/dist/extraction-text.js +45 -0
- package/dist/extraction-text.js.map +1 -0
- package/dist/extraction-values.d.ts +40 -0
- package/dist/extraction-values.js +30 -0
- package/dist/extraction-values.js.map +1 -0
- package/dist/extraction-worker.d.ts +1 -0
- package/dist/extraction-worker.js +26 -0
- package/dist/extraction-worker.js.map +1 -0
- package/dist/extraction.d.ts +7 -0
- package/dist/extraction.js +270 -0
- package/dist/extraction.js.map +1 -0
- package/dist/healthcheck.d.ts +6 -0
- package/dist/healthcheck.js +90 -0
- package/dist/healthcheck.js.map +1 -0
- package/dist/http/adapter.d.ts +24 -0
- package/dist/http/adapter.js +139 -0
- package/dist/http/adapter.js.map +1 -0
- package/dist/http/checks.d.ts +23 -0
- package/dist/http/checks.js +90 -0
- package/dist/http/checks.js.map +1 -0
- package/dist/http/errors.d.ts +8 -0
- package/dist/http/errors.js +26 -0
- package/dist/http/errors.js.map +1 -0
- package/dist/http/handler.d.ts +3 -0
- package/dist/http/handler.js +543 -0
- package/dist/http/handler.js.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +4 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/limits.d.ts +37 -0
- package/dist/http/limits.js +56 -0
- package/dist/http/limits.js.map +1 -0
- package/dist/http/responses.d.ts +5 -0
- package/dist/http/responses.js +20 -0
- package/dist/http/responses.js.map +1 -0
- package/dist/http/server.d.ts +18 -0
- package/dist/http/server.js +97 -0
- package/dist/http/server.js.map +1 -0
- package/dist/http/sessions.d.ts +45 -0
- package/dist/http/sessions.js +80 -0
- package/dist/http/sessions.js.map +1 -0
- package/dist/http/types.d.ts +37 -0
- package/dist/http/types.js +2 -0
- package/dist/http/types.js.map +1 -0
- package/dist/kdrive/adapter.d.ts +28 -0
- package/dist/kdrive/adapter.js +479 -0
- package/dist/kdrive/adapter.js.map +1 -0
- package/dist/kdrive/address.d.ts +2 -0
- package/dist/kdrive/address.js +88 -0
- package/dist/kdrive/address.js.map +1 -0
- package/dist/kdrive/client.d.ts +7 -0
- package/dist/kdrive/client.js +16 -0
- package/dist/kdrive/client.js.map +1 -0
- package/dist/kdrive/index.d.ts +3 -0
- package/dist/kdrive/index.js +3 -0
- package/dist/kdrive/index.js.map +1 -0
- package/dist/kdrive/mutators.d.ts +7 -0
- package/dist/kdrive/mutators.js +194 -0
- package/dist/kdrive/mutators.js.map +1 -0
- package/dist/kdrive/normalize.d.ts +91 -0
- package/dist/kdrive/normalize.js +285 -0
- package/dist/kdrive/normalize.js.map +1 -0
- package/dist/kdrive/production.d.ts +9 -0
- package/dist/kdrive/production.js +16 -0
- package/dist/kdrive/production.js.map +1 -0
- package/dist/kdrive/routes.d.ts +38 -0
- package/dist/kdrive/routes.js +89 -0
- package/dist/kdrive/routes.js.map +1 -0
- package/dist/kdrive/transport.d.ts +92 -0
- package/dist/kdrive/transport.js +810 -0
- package/dist/kdrive/transport.js.map +1 -0
- package/dist/main.d.ts +52 -0
- package/dist/main.js +220 -0
- package/dist/main.js.map +1 -0
- package/dist/mutation-gate.d.ts +65 -0
- package/dist/mutation-gate.js +439 -0
- package/dist/mutation-gate.js.map +1 -0
- package/dist/mutation-tools.d.ts +73 -0
- package/dist/mutation-tools.js +203 -0
- package/dist/mutation-tools.js.map +1 -0
- package/dist/navigation.d.ts +45 -0
- package/dist/navigation.js +680 -0
- package/dist/navigation.js.map +1 -0
- package/dist/pagination.d.ts +29 -0
- package/dist/pagination.js +192 -0
- package/dist/pagination.js.map +1 -0
- package/dist/pdf-syntax.d.ts +39 -0
- package/dist/pdf-syntax.js +292 -0
- package/dist/pdf-syntax.js.map +1 -0
- package/dist/policy/glob.d.ts +1 -0
- package/dist/policy/glob.js +36 -0
- package/dist/policy/glob.js.map +1 -0
- package/dist/policy/index.d.ts +88 -0
- package/dist/policy/index.js +671 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/operation.d.ts +17 -0
- package/dist/policy/operation.js +86 -0
- package/dist/policy/operation.js.map +1 -0
- package/dist/policy/received.d.ts +70 -0
- package/dist/policy/received.js +412 -0
- package/dist/policy/received.js.map +1 -0
- package/dist/policy/values.d.ts +46 -0
- package/dist/policy/values.js +182 -0
- package/dist/policy/values.js.map +1 -0
- package/dist/production.d.ts +3 -0
- package/dist/production.js +23 -0
- package/dist/production.js.map +1 -0
- package/dist/read-retry.d.ts +6 -0
- package/dist/read-retry.js +45 -0
- package/dist/read-retry.js.map +1 -0
- package/dist/stdio-transport.d.ts +9 -0
- package/dist/stdio-transport.js +289 -0
- package/dist/stdio-transport.js.map +1 -0
- package/dist/stdio.d.ts +19 -0
- package/dist/stdio.js +78 -0
- package/dist/stdio.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +65 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sstolz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
kdrive-mcp-server is an unofficial project, not affiliated with Infomaniak.
|
|
2
|
+
Original project code is licensed under the MIT License in LICENSE.
|
|
3
|
+
|
|
4
|
+
Upstream comparison baseline:
|
|
5
|
+
https://github.com/Infomaniak/mcp-server-kdrive
|
|
6
|
+
Tag: 1.2.0
|
|
7
|
+
Pinned revision: 857272d7fd42c0940ae389b9092b1c4cd2351337
|
|
8
|
+
Reference file: src/kdrive-client.ts
|
|
9
|
+
|
|
10
|
+
The pinned upstream package.json and README declare MIT. The examined pinned
|
|
11
|
+
root tree has no standalone LICENSE file, and the reference source has no
|
|
12
|
+
copyright header. No upstream copyright statement is invented here.
|
|
13
|
+
No upstream source code is copied into this foundation. Future adaptation in
|
|
14
|
+
P-05 requires resolving provenance, comparing this pinned source with the then
|
|
15
|
+
current upstream revision, and recording retained and changed behavior.
|
|
16
|
+
|
|
17
|
+
Dependency notice:
|
|
18
|
+
@modelcontextprotocol/server is licensed under Apache-2.0. This foundation
|
|
19
|
+
installs the official SDK without copying or bundling its source. Dependencies
|
|
20
|
+
retain their own license and notice files in their separately installed packages.
|
package/README.md
CHANGED
|
@@ -1,3 +1,302 @@
|
|
|
1
|
-
#
|
|
1
|
+
# kdrive-mcp-server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Beta: the interface may change before 1.0.** Tool names, parameters,
|
|
4
|
+
> settings and outputs can still change between beta versions.
|
|
5
|
+
>
|
|
6
|
+
> **Unofficial project, not affiliated with Infomaniak.** It is distinct from
|
|
7
|
+
> the `@infomaniak/mcp-server-kdrive` npm package.
|
|
8
|
+
|
|
9
|
+
An [MCP](https://modelcontextprotocol.io) server that gives AI clients
|
|
10
|
+
controlled access to an Infomaniak kDrive. A client can browse, search and read
|
|
11
|
+
files under the folders you configure; in `write` mode it can also create,
|
|
12
|
+
rename, move, trash, restore and upload. Every call goes through a policy
|
|
13
|
+
(configured roots, deny patterns, received-share rules) and every write is
|
|
14
|
+
recorded in an audit log before and after it is sent.
|
|
15
|
+
|
|
16
|
+
**Status:** beta `0.6.0-beta.1` ([VERSION](VERSION)), to be published through
|
|
17
|
+
the gated release workflow described in [docs/publishing.md](docs/publishing.md)
|
|
18
|
+
as npm `@sstolz/kdrive-mcp-server@beta` and image
|
|
19
|
+
`ghcr.io/sstolz/kdrive-mcp-server:0.6.0-beta.1`. You can also build the image or
|
|
20
|
+
the package from this repository. Changes are listed in [CHANGELOG.md](CHANGELOG.md).
|
|
21
|
+
|
|
22
|
+
## What it offers
|
|
23
|
+
|
|
24
|
+
- **Three modes**, cumulative: `read` (default), `write`, `share`. Today `share`
|
|
25
|
+
adds nothing beyond `write`; sharing tools are planned.
|
|
26
|
+
- **Two transports:** stdio for a local client, and authenticated Streamable
|
|
27
|
+
HTTP (OAuth proxy) for a remote connector.
|
|
28
|
+
- **Scoped access:** only configured drives and root folders are visible; deny
|
|
29
|
+
patterns hide paths; items shared with you are off unless you opt in.
|
|
30
|
+
- **Bounded results:** reads, downloads and listings have size, item and time
|
|
31
|
+
limits; names, paths and file content are returned as untrusted data.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
Prerequisites:
|
|
36
|
+
|
|
37
|
+
- an Infomaniak API token for kDrive, from an account with the **admin** role on
|
|
38
|
+
every drive you configure (startup checks this);
|
|
39
|
+
- Docker with Compose and GNU Make 3.81 or later to build from this repository;
|
|
40
|
+
- for the packaged CLI only: Node.js 26.13 or later (26.x).
|
|
41
|
+
|
|
42
|
+
Build from a checkout of this repository:
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
make setup # build the development container and install dependencies
|
|
46
|
+
make image # container image: kdrive-mcp-server:runtime-<arch>
|
|
47
|
+
make pack # npm package: ./artifacts/sstolz-kdrive-mcp-server-0.6.0-beta.1.tgz
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`make image IMAGE_PLATFORMS=linux/amd64` builds one architecture only. Nothing
|
|
51
|
+
is pushed to a registry. To use the CLI on your machine, install the package
|
|
52
|
+
with `npm install -g ./artifacts/sstolz-kdrive-mcp-server-0.6.0-beta.1.tgz` (keep the
|
|
53
|
+
leading `./`, without which npm reads the argument as a GitHub shorthand); it
|
|
54
|
+
provides the `kdrive-mcp-server` command. `kdrive-mcp-server --help` and
|
|
55
|
+
`--version` need no configuration.
|
|
56
|
+
|
|
57
|
+
Once the public beta is published, install it from the npm registry with
|
|
58
|
+
`npm install -g @sstolz/kdrive-mcp-server@beta`
|
|
59
|
+
([publishing](docs/publishing.md)).
|
|
60
|
+
|
|
61
|
+
## Configure
|
|
62
|
+
|
|
63
|
+
Settings come from, in priority order: CLI options, environment variables, one
|
|
64
|
+
YAML file selected with `--config PATH`, then defaults. No `.env` file is read.
|
|
65
|
+
The essential settings:
|
|
66
|
+
|
|
67
|
+
| Environment variable | Purpose | Default |
|
|
68
|
+
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
|
69
|
+
| `KDRIVE_TOKEN_FILE` | Absolute path of a file holding the kDrive API token (preferred). `KDRIVE_TOKEN` passes it inline. | — |
|
|
70
|
+
| `KDRIVE_ID` / `KDRIVE_IDS` | One drive ID, or a comma-separated list whose first entry is the default drive. | — |
|
|
71
|
+
| `KDRIVE_MCP_MODE` | `read`, `write` or `share`. | `read` |
|
|
72
|
+
| `KDRIVE_MCP_ROOTS` | Root folders per drive, by ID or path, for example `7:42,8:/Documents`. Must cover every configured drive. | folder 1 per drive |
|
|
73
|
+
| `KDRIVE_MCP_DENY` | Comma-separated path patterns to hide, for example `/Private/**`. Case-sensitive; see [Limits and risks](#limits-and-risks). | none |
|
|
74
|
+
| `KDRIVE_MCP_AUDIT_LOG` | `stderr` or an absolute file path for the write audit log (`write` and `share` modes). | `stderr` |
|
|
75
|
+
| `KDRIVE_MCP_SHARED_WITH_ME` | `1` to read items other users shared with you (`0` or `1`). | `0` |
|
|
76
|
+
| `KDRIVE_MCP_TRANSPORT` | `stdio` or `http`. | `stdio` |
|
|
77
|
+
|
|
78
|
+
Never put a token in a CLI argument or in the YAML file. Size limits
|
|
79
|
+
(`KDRIVE_MCP_MAX_READ_BYTES`, `KDRIVE_MCP_MAX_OUTPUT_CHARS`, …), the HTTP and
|
|
80
|
+
OAuth settings, every CLI option and YAML key, defaults, bounds and error codes
|
|
81
|
+
are in the [configuration reference](features/F-002-configuration.md). A
|
|
82
|
+
configuration or startup failure prints one line,
|
|
83
|
+
`Configuration error [<code>] (<setting>).`, and exits 1.
|
|
84
|
+
|
|
85
|
+
## Connect an AI client
|
|
86
|
+
|
|
87
|
+
### Local client (stdio)
|
|
88
|
+
|
|
89
|
+
Most desktop clients and coding assistants start local MCP servers from a JSON
|
|
90
|
+
configuration of this shape (see your client's documentation for the file
|
|
91
|
+
location):
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpServers": {
|
|
96
|
+
"kdrive": {
|
|
97
|
+
"command": "kdrive-mcp-server",
|
|
98
|
+
"env": {
|
|
99
|
+
"KDRIVE_TOKEN_FILE": "/home/me/.config/kdrive/token",
|
|
100
|
+
"KDRIVE_ID": "123456",
|
|
101
|
+
"KDRIVE_MCP_ROOTS": "123456:/Documents",
|
|
102
|
+
"KDRIVE_MCP_MODE": "read"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The server speaks MCP on stdin and stdout and writes its own diagnostics to
|
|
110
|
+
stderr.
|
|
111
|
+
|
|
112
|
+
### Remote connector (authenticated HTTP)
|
|
113
|
+
|
|
114
|
+
For web and mobile clients, run the container image behind a TLS reverse proxy.
|
|
115
|
+
The server acts as an OAuth proxy in front of an OpenID Connect provider (for
|
|
116
|
+
example Infomaniak's) and only accepts the subjects or emails you allow.
|
|
117
|
+
|
|
118
|
+
1. Register an OAuth client at your provider with the redirect URI
|
|
119
|
+
`<MCP_PUBLIC_URL>/oauth/callback`, for example
|
|
120
|
+
`https://mcp.example.com/mcp/oauth/callback`.
|
|
121
|
+
2. Provision three secret files: the kDrive token, the upstream client secret,
|
|
122
|
+
and an EC P-256 signing key in PKCS#8 PEM (the server never generates one).
|
|
123
|
+
3. Start from [examples/compose.yaml](examples/compose.yaml) (environment and
|
|
124
|
+
Compose secrets behind Traefik) or [examples/config.yaml](examples/config.yaml)
|
|
125
|
+
(YAML alternative). The key settings:
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
KDRIVE_MCP_TRANSPORT: http
|
|
129
|
+
OAUTH_MODE: proxy
|
|
130
|
+
MCP_HTTP_HOST: 0.0.0.0 # inside the container only
|
|
131
|
+
MCP_PUBLIC_URL: https://mcp.example.com/mcp
|
|
132
|
+
OAUTH_ISSUER: https://login.example.com # your OIDC provider
|
|
133
|
+
OAUTH_UPSTREAM_CLIENT_ID: <client id>
|
|
134
|
+
OAUTH_ALLOWED_EMAILS: you@example.com
|
|
135
|
+
OAUTH_ALLOWED_REDIRECT_URIS: https://claude.ai/api/mcp/auth_callback
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
4. In the client, add a custom connector with the URL `MCP_PUBLIC_URL`. The
|
|
139
|
+
client registers itself, you log in at your provider, and the server checks
|
|
140
|
+
your identity against the allowlist.
|
|
141
|
+
|
|
142
|
+
`OAUTH_ALLOWED_REDIRECT_URIS` lists the callback URLs of the clients you use;
|
|
143
|
+
the [client compatibility matrix](docs/mcp-client-compatibility.md) gives them
|
|
144
|
+
for common clients. Operator details (signing key, redirect matching, restart
|
|
145
|
+
behaviour, reverse proxy) are in the [OAuth proxy notes](docs/oauth-proxy.md)
|
|
146
|
+
and the [deployment guide](docs/deployment.md), which walks through a personal
|
|
147
|
+
server installation step by step. In `write` and `share` modes a
|
|
148
|
+
file audit log needs a writable mount, and `stop_grace_period` must be at least
|
|
149
|
+
75 s so in-flight writes can record their outcome.
|
|
150
|
+
|
|
151
|
+
## Tools
|
|
152
|
+
|
|
153
|
+
Item metadata has exactly `drive_id`, `id`, `name`, `type` and `path`.
|
|
154
|
+
|
|
155
|
+
Read tools (all modes):
|
|
156
|
+
|
|
157
|
+
| Tool | Purpose |
|
|
158
|
+
| ----------------------- | ------------------------------------------------------------------------------ |
|
|
159
|
+
| `kdrive_list_drives` | List the configured drives. |
|
|
160
|
+
| `kdrive_list` | List a folder's children, with optional `type` filter and continuation. |
|
|
161
|
+
| `kdrive_info` | Metadata of one item. |
|
|
162
|
+
| `kdrive_resolve_path` | Find an item by its path. |
|
|
163
|
+
| `kdrive_search` | Search names under the roots or one folder (`query` required). |
|
|
164
|
+
| `kdrive_recents` | Recently changed items under the roots or one folder. |
|
|
165
|
+
| `kdrive_shared_with_me` | Items other users shared with you (needs `KDRIVE_MCP_SHARED_WITH_ME`). |
|
|
166
|
+
| `kdrive_my_shared` | Items you shared. |
|
|
167
|
+
| `kdrive_read` | Extract text (UTF-8/16, Windows-1252), PDF text, or a PNG preview of an image. |
|
|
168
|
+
| `kdrive_download` | Raw bytes as base64 chunks, continued with `offset_bytes` and `etag`. |
|
|
169
|
+
|
|
170
|
+
Resource template: `kdrive://{drive_id}/file/{id}` returns one complete binary
|
|
171
|
+
file (`application/octet-stream`) when it fits the read limit.
|
|
172
|
+
|
|
173
|
+
Write tools (`write` and `share` modes only):
|
|
174
|
+
|
|
175
|
+
| Tool | Purpose |
|
|
176
|
+
| ---------------------- | ---------------------------------------------------------------- |
|
|
177
|
+
| `kdrive_create_folder` | Create a folder. |
|
|
178
|
+
| `kdrive_rename` | Rename an item. |
|
|
179
|
+
| `kdrive_move` | Move an item to another folder of the same drive. |
|
|
180
|
+
| `kdrive_trash` | Move an item to the kDrive trash (never a permanent delete). |
|
|
181
|
+
| `kdrive_restore` | Restore a trashed item to its former parent. |
|
|
182
|
+
| `kdrive_upload` | Upload `text` or `base64` content, new file or in-place replace. |
|
|
183
|
+
|
|
184
|
+
Each write returns `succeeded`, `failed`, `uncertain` or `denied`. After
|
|
185
|
+
`uncertain`, check the item with a read tool before trying again: the server
|
|
186
|
+
never replays an uncertain write.
|
|
187
|
+
|
|
188
|
+
Key limits:
|
|
189
|
+
|
|
190
|
+
- `kdrive_upload` accepts at most 524,288 decoded bytes; over HTTP the request
|
|
191
|
+
body limit is 262,144 bytes. Use `base64` for large or non-ASCII content.
|
|
192
|
+
- Names given to `kdrive_upload`, `kdrive_create_folder` and `kdrive_rename`
|
|
193
|
+
must be in Unicode NFC (composed form); other forms are refused with
|
|
194
|
+
`INVALID_ARGUMENTS`.
|
|
195
|
+
- `kdrive_read` handles a conservative PDF profile only, so many valid PDFs
|
|
196
|
+
return `UNSUPPORTED_FORMAT`; images are returned within 512x512 pixels.
|
|
197
|
+
- Listings return at most 256 items per call and continue with single-use
|
|
198
|
+
cursors that expire after five minutes; downloads continue with
|
|
199
|
+
`offset_bytes` and the previous chunk's `etag`.
|
|
200
|
+
|
|
201
|
+
The full read behaviour (pagination, encodings, PDF profile, bounds) is
|
|
202
|
+
described in [docs/development.md](docs/development.md).
|
|
203
|
+
|
|
204
|
+
## Limits and risks
|
|
205
|
+
|
|
206
|
+
The write tools exist only in `write` and `share` modes. Read
|
|
207
|
+
[SECURITY.md](SECURITY.md) for the full security boundary.
|
|
208
|
+
|
|
209
|
+
Tree writes (create, rename, move, trash, restore):
|
|
210
|
+
|
|
211
|
+
- This server never requests permanent deletion: `kdrive_trash` moves an item
|
|
212
|
+
to the kDrive trash and no route empties the trash or deletes for good.
|
|
213
|
+
- `kdrive_restore` returns an item only to its former parent, and only when
|
|
214
|
+
that parent is live and authorized; otherwise the restore is refused. A
|
|
215
|
+
folder restore is refused while deny patterns are configured (the trashed
|
|
216
|
+
tree cannot be enumerated).
|
|
217
|
+
- Configured roots cannot be trashed, renamed or moved.
|
|
218
|
+
- Received-share writes stay refused, even with `KDRIVE_MCP_SHARED_WRITE=1`.
|
|
219
|
+
- A check-then-act window remains (SPEC §10.6): the drive can change between
|
|
220
|
+
the check and the write. If the former parent is trashed between the check
|
|
221
|
+
and a restore, kDrive recreates a folder at the former path and restores into
|
|
222
|
+
it; when that former parent is a configured root, the recreated folder has a
|
|
223
|
+
new ID outside every configured root. If an item is trashed between the check
|
|
224
|
+
and a trash request, the effect of `DELETE /2/drive/{d}/files/{f}` on the
|
|
225
|
+
already trashed item is unmeasured.
|
|
226
|
+
- Residual risk R-17-1 (not observed on the measured server): restore
|
|
227
|
+
authorizes the former parent's current path, so an item trashed under a denied
|
|
228
|
+
folder that is renamed or moved out of the deny patterns outside this server
|
|
229
|
+
could be restored if kDrive recomputed the trash `path`; owner acceptance on
|
|
230
|
+
2026-11-10 observed a deletion-time snapshot, so such a restore was denied.
|
|
231
|
+
- Residual risk R-17-2: the root protection covers only a configured root
|
|
232
|
+
targeted directly. With nested roots, trashing, renaming or moving an
|
|
233
|
+
authorized folder of an outer root that contains an inner configured root
|
|
234
|
+
also affects that inner root, and with deny patterns the trashed folder
|
|
235
|
+
cannot be restored through this server.
|
|
236
|
+
|
|
237
|
+
Upload (`kdrive_upload`):
|
|
238
|
+
|
|
239
|
+
- `overwrite:false` (the default) asks kDrive to refuse an existing file of
|
|
240
|
+
the same name (`conflict=error`). That refusal is observed kDrive behaviour
|
|
241
|
+
(9 of 9 measured samples, checked when the upload completes), not a
|
|
242
|
+
guarantee of this server. Names are case-sensitive.
|
|
243
|
+
- `overwrite:true` requires `file_id`, the existing file called `name` in
|
|
244
|
+
`parent_id`; its content is replaced in place under the same ID. An existing
|
|
245
|
+
file whose name is not NFC must be renamed to its NFC name before it can be
|
|
246
|
+
overwritten.
|
|
247
|
+
- An `uncertain` upload is never replayed; check the file with a read tool
|
|
248
|
+
before any new attempt.
|
|
249
|
+
- Received-share uploads stay refused, even with `KDRIVE_MCP_SHARED_WRITE=1`.
|
|
250
|
+
- The 524,288-byte bound applies to the decoded bytes, while the transport
|
|
251
|
+
limit (1 MiB stdio frame, 262,144-byte HTTP body) applies to the
|
|
252
|
+
JSON-encoded request. `text` that JSON escaping enlarges can be refused by
|
|
253
|
+
the transport below 524,288 bytes: over HTTP the request gets HTTP 413; over
|
|
254
|
+
stdio a frame above 1 MiB terminates the server with `STDIO_INPUT_FAILED`.
|
|
255
|
+
An upload already in flight completes and its outcome is recorded in the
|
|
256
|
+
audit log before exit, but its tool result is not delivered.
|
|
257
|
+
- Residual risks R-18-1 to R-18-5: the `conflict=error` refusal is not proven
|
|
258
|
+
under every interleaving (R-18-1); between the check and a replace the target
|
|
259
|
+
can change, giving `uncertain`, an undetected deny-pattern rename, or
|
|
260
|
+
`succeeded` for a destination moved or trashed out of the configured roots
|
|
261
|
+
(R-18-2);
|
|
262
|
+
kDrive stores a file named like an existing folder under another name, not
|
|
263
|
+
checked against the deny patterns (R-18-3, accepted); kDrive's own version
|
|
264
|
+
history decides whether replaced content is kept (R-18-4); untested name
|
|
265
|
+
rewriting by kDrive could store a file at an unchecked path, reported
|
|
266
|
+
`uncertain` for upload and folder creation but `succeeded` for rename
|
|
267
|
+
(R-18-5). Details: [SECURITY.md](SECURITY.md) and
|
|
268
|
+
[docs/development.md](docs/development.md#upload).
|
|
269
|
+
|
|
270
|
+
Deny patterns:
|
|
271
|
+
|
|
272
|
+
- Patterns match full, case-sensitive paths. `/private/**` covers the folder
|
|
273
|
+
and everything below it; `/private` matches only that exact path.
|
|
274
|
+
- Patterns match the exact stored code points. Write the NFC form of a
|
|
275
|
+
non-ASCII pattern to cover names these tools create, and keep (or add) the
|
|
276
|
+
stored form for any existing item whose name is not NFC; replacing a
|
|
277
|
+
working NFD pattern by its NFC form alone would drop the deny for those
|
|
278
|
+
items, reads included.
|
|
279
|
+
|
|
280
|
+
Audit log: each write gets one JSON line before it is sent and one after; if
|
|
281
|
+
the first cannot be written, nothing is sent. Records hold IDs, never names,
|
|
282
|
+
paths, content or secrets. With `stderr`, records are only as durable as what
|
|
283
|
+
collects stderr; use a file for a durable trail.
|
|
284
|
+
|
|
285
|
+
## More documentation
|
|
286
|
+
|
|
287
|
+
- [SECURITY.md](SECURITY.md): security boundary and how to report a
|
|
288
|
+
vulnerability.
|
|
289
|
+
- [Configuration reference](features/F-002-configuration.md),
|
|
290
|
+
[deployment guide](docs/deployment.md), [OAuth proxy notes](docs/oauth-proxy.md).
|
|
291
|
+
- [Publishing](docs/publishing.md): package and image names, versions and the
|
|
292
|
+
release gate.
|
|
293
|
+
- [Development notes](docs/development.md): building, checks, quality gates and
|
|
294
|
+
feature history, until the developer documentation exists.
|
|
295
|
+
- Contributors follow [AGENTS.md](AGENTS.md) and [PROJECT.md](PROJECT.md).
|
|
296
|
+
This project is developed with AI coding agents under the maintainer's
|
|
297
|
+
direction and review; see AGENTS.md.
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
Original project code uses the [MIT license](LICENSE). [NOTICE](NOTICE) records
|
|
302
|
+
the upstream comparison baseline and dependency licensing.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** BR-237: the identity of the caller of a gate run. */
|
|
2
|
+
export type AuditIdentity = {
|
|
3
|
+
readonly kind: "stdio";
|
|
4
|
+
} | {
|
|
5
|
+
readonly kind: "insecure";
|
|
6
|
+
} | {
|
|
7
|
+
readonly kind: "oauth";
|
|
8
|
+
readonly iss: string;
|
|
9
|
+
readonly sub: string;
|
|
10
|
+
};
|
|
11
|
+
export type AuditEvent = "intent" | "outcome" | "denied";
|
|
12
|
+
export type AuditResult = "succeeded" | "failed" | "uncertain";
|
|
13
|
+
/** F-016 closed code set (record schema). */
|
|
14
|
+
export type AuditCode = "ACCESS_DENIED" | "AUDIT_UNAVAILABLE" | "UNAVAILABLE" | "CANCELLED" | "RATE_LIMITED" | "NOT_SENT" | "UPSTREAM_REJECTED" | "UPSTREAM_UNCERTAIN";
|
|
15
|
+
export interface AuditIds {
|
|
16
|
+
readonly target?: number;
|
|
17
|
+
readonly destination?: number;
|
|
18
|
+
}
|
|
19
|
+
/** BR-234: the members of one record; `serializeAuditRecord` fixes their order. */
|
|
20
|
+
export interface AuditRecord {
|
|
21
|
+
readonly ts: string;
|
|
22
|
+
readonly event: AuditEvent;
|
|
23
|
+
readonly op: string;
|
|
24
|
+
readonly identity: AuditIdentity;
|
|
25
|
+
readonly tool: string;
|
|
26
|
+
readonly kind: string;
|
|
27
|
+
readonly drive: number;
|
|
28
|
+
readonly destinationDrive?: number;
|
|
29
|
+
readonly ids: AuditIds;
|
|
30
|
+
readonly result?: AuditResult;
|
|
31
|
+
readonly attempts?: number;
|
|
32
|
+
readonly ms?: number;
|
|
33
|
+
readonly status?: number;
|
|
34
|
+
readonly code?: AuditCode;
|
|
35
|
+
readonly created?: number;
|
|
36
|
+
}
|
|
37
|
+
/** BR-236: the largest serialized record, LF included. */
|
|
38
|
+
export declare const AUDIT_RECORD_MAX_BYTES = 8192;
|
|
39
|
+
/** BR-259: the file-sink append bound. */
|
|
40
|
+
export declare const AUDIT_APPEND_TIMEOUT_MS = 5000;
|
|
41
|
+
/** BR-239: the fixed startup refusal. */
|
|
42
|
+
export declare const AUDIT_OPEN_FAILED_MESSAGE = "The audit log could not be opened or is not a private regular file.";
|
|
43
|
+
/** A fixed, content-free append failure. */
|
|
44
|
+
export declare class AuditUnavailable extends Error {
|
|
45
|
+
constructor();
|
|
46
|
+
}
|
|
47
|
+
/** BR-239: startup stops with this fixed message. */
|
|
48
|
+
export declare class AuditStartupError extends Error {
|
|
49
|
+
constructor();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* BR-234/BR-236: one JSON object and one LF. Every code unit outside
|
|
53
|
+
* U+0020–U+007E is escaped as `\uXXXX`, so the line is printable ASCII.
|
|
54
|
+
*/
|
|
55
|
+
export declare function serializeAuditRecord(record: AuditRecord): string;
|
|
56
|
+
export interface AuditSink {
|
|
57
|
+
/** BR-240/BR-242: resolves once the whole record is durable per the sink. */
|
|
58
|
+
append(record: AuditRecord): Promise<void>;
|
|
59
|
+
readonly latched: boolean;
|
|
60
|
+
/** BR-241/BR-252: refuses every later append without I/O. */
|
|
61
|
+
latch(): void;
|
|
62
|
+
close(): Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/** The file operations of the file sink; injected by tests. */
|
|
65
|
+
export interface AuditFilePort {
|
|
66
|
+
open(path: string, flags: number, mode: number): Promise<number>;
|
|
67
|
+
fstat(fd: number): Promise<{
|
|
68
|
+
isFile(): boolean;
|
|
69
|
+
uid: number;
|
|
70
|
+
mode: number;
|
|
71
|
+
}>;
|
|
72
|
+
geteuid(): number;
|
|
73
|
+
/** Resolves with the number of bytes written by one write call. */
|
|
74
|
+
write(fd: number, data: Uint8Array): Promise<number>;
|
|
75
|
+
fdatasync(fd: number): Promise<void>;
|
|
76
|
+
close(fd: number): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
export declare const productionFilePort: AuditFilePort;
|
|
79
|
+
/**
|
|
80
|
+
* BR-239 flags. O_NONBLOCK makes an open of a FIFO without reader fail instead
|
|
81
|
+
* of blocking startup; it has no effect on a regular file.
|
|
82
|
+
*/
|
|
83
|
+
export declare const AUDIT_OPEN_FLAGS: number;
|
|
84
|
+
export interface FileSinkOptions {
|
|
85
|
+
readonly port?: AuditFilePort;
|
|
86
|
+
/** BR-259 timer; defaults to the global timers. */
|
|
87
|
+
readonly timeoutMs?: number;
|
|
88
|
+
}
|
|
89
|
+
/** BR-239–BR-242, BR-259: the private append-only file sink. */
|
|
90
|
+
export declare function openFileSink(path: string, options?: FileSinkOptions): Promise<AuditSink>;
|
|
91
|
+
/** The part of a writable stream the stderr sink uses. */
|
|
92
|
+
export interface AuditStream {
|
|
93
|
+
write(chunk: string, callback: (error?: Error | null) => void): unknown;
|
|
94
|
+
on?(event: "error", listener: (error: Error) => void): unknown;
|
|
95
|
+
}
|
|
96
|
+
/** BR-238/BR-240/BR-241: records on file descriptor 2, resolved by the write callback. */
|
|
97
|
+
export declare function createStderrSink(stream: AuditStream): AuditSink;
|
|
98
|
+
/** BR-238: `stderr` or an absolute file path (F-002). */
|
|
99
|
+
export declare function openAuditSink(auditLog: string, options: {
|
|
100
|
+
readonly stderr: AuditStream;
|
|
101
|
+
readonly port?: AuditFilePort;
|
|
102
|
+
}): Promise<AuditSink>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { constants, close, fdatasync, fstat, open, write } from "node:fs";
|
|
2
|
+
/** BR-236: the largest serialized record, LF included. */
|
|
3
|
+
export const AUDIT_RECORD_MAX_BYTES = 8192;
|
|
4
|
+
/** BR-259: the file-sink append bound. */
|
|
5
|
+
export const AUDIT_APPEND_TIMEOUT_MS = 5000;
|
|
6
|
+
/** BR-239: the fixed startup refusal. */
|
|
7
|
+
export const AUDIT_OPEN_FAILED_MESSAGE = "The audit log could not be opened or is not a private regular file.";
|
|
8
|
+
/** A fixed, content-free append failure. */
|
|
9
|
+
export class AuditUnavailable extends Error {
|
|
10
|
+
constructor() {
|
|
11
|
+
super("AUDIT_UNAVAILABLE");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** BR-239: startup stops with this fixed message. */
|
|
15
|
+
export class AuditStartupError extends Error {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(AUDIT_OPEN_FAILED_MESSAGE);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function identityValue(identity) {
|
|
21
|
+
return identity.kind === "oauth"
|
|
22
|
+
? { kind: "oauth", iss: identity.iss, sub: identity.sub }
|
|
23
|
+
: { kind: identity.kind };
|
|
24
|
+
}
|
|
25
|
+
function idsValue(ids) {
|
|
26
|
+
return {
|
|
27
|
+
...(ids.target === undefined ? {} : { target: ids.target }),
|
|
28
|
+
...(ids.destination === undefined ? {} : { destination: ids.destination }),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function eventMembers(record) {
|
|
32
|
+
if (record.event === "denied")
|
|
33
|
+
return { code: record.code };
|
|
34
|
+
if (record.event === "intent")
|
|
35
|
+
return {};
|
|
36
|
+
return {
|
|
37
|
+
result: record.result,
|
|
38
|
+
attempts: record.attempts,
|
|
39
|
+
ms: record.ms,
|
|
40
|
+
...(record.status === undefined ? {} : { status: record.status }),
|
|
41
|
+
...(record.code === undefined ? {} : { code: record.code }),
|
|
42
|
+
...(record.created === undefined ? {} : { created: record.created }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* BR-234/BR-236: one JSON object and one LF. Every code unit outside
|
|
47
|
+
* U+0020–U+007E is escaped as `\uXXXX`, so the line is printable ASCII.
|
|
48
|
+
*/
|
|
49
|
+
export function serializeAuditRecord(record) {
|
|
50
|
+
const json = JSON.stringify({
|
|
51
|
+
v: 1,
|
|
52
|
+
ts: record.ts,
|
|
53
|
+
event: record.event,
|
|
54
|
+
op: record.op,
|
|
55
|
+
identity: identityValue(record.identity),
|
|
56
|
+
tool: record.tool,
|
|
57
|
+
kind: record.kind,
|
|
58
|
+
drive: record.drive,
|
|
59
|
+
...(record.destinationDrive === undefined
|
|
60
|
+
? {}
|
|
61
|
+
: { destinationDrive: record.destinationDrive }),
|
|
62
|
+
ids: idsValue(record.ids),
|
|
63
|
+
...eventMembers(record),
|
|
64
|
+
});
|
|
65
|
+
// BR-236: without the `u` flag each match is one UTF-16 code unit, so
|
|
66
|
+
// `codePointAt(0)` yields that unit (a lone surrogate stays a surrogate).
|
|
67
|
+
const line = json.replaceAll(/[^\x20-\x7e]/g, (unit) => `\\u${(unit.codePointAt(0) ?? 0).toString(16).padStart(4, "0")}`);
|
|
68
|
+
return `${line}\n`;
|
|
69
|
+
}
|
|
70
|
+
/** BR-241/BR-242: one append at a time, in call order, latched on failure. */
|
|
71
|
+
function serialSink(writeLine, release) {
|
|
72
|
+
let latched = false, closed = false, tail = Promise.resolve();
|
|
73
|
+
const sink = {
|
|
74
|
+
get latched() {
|
|
75
|
+
return latched;
|
|
76
|
+
},
|
|
77
|
+
latch() {
|
|
78
|
+
latched = true;
|
|
79
|
+
},
|
|
80
|
+
append(record) {
|
|
81
|
+
const run = tail.then(async () => {
|
|
82
|
+
if (latched || closed)
|
|
83
|
+
throw new AuditUnavailable();
|
|
84
|
+
const line = serializeAuditRecord(record);
|
|
85
|
+
// BR-236: an oversize record rejects without I/O and without latching.
|
|
86
|
+
if (Buffer.byteLength(line) > AUDIT_RECORD_MAX_BYTES)
|
|
87
|
+
throw new AuditUnavailable();
|
|
88
|
+
try {
|
|
89
|
+
await writeLine(line);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
latched = true;
|
|
93
|
+
throw new AuditUnavailable();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
tail = run.catch(() => undefined);
|
|
97
|
+
return run;
|
|
98
|
+
},
|
|
99
|
+
async close() {
|
|
100
|
+
if (closed)
|
|
101
|
+
return;
|
|
102
|
+
closed = true;
|
|
103
|
+
await tail;
|
|
104
|
+
await release();
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
return sink;
|
|
108
|
+
}
|
|
109
|
+
function settle(resolve, reject) {
|
|
110
|
+
return (error, value) => {
|
|
111
|
+
if (error)
|
|
112
|
+
reject(error);
|
|
113
|
+
else
|
|
114
|
+
resolve(value);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export const productionFilePort = Object.freeze({
|
|
118
|
+
open: (path, flags, mode) => new Promise((resolve, reject) => {
|
|
119
|
+
open(path, flags, mode, settle(resolve, reject));
|
|
120
|
+
}),
|
|
121
|
+
fstat: (fd) => new Promise((resolve, reject) => {
|
|
122
|
+
fstat(fd, settle(resolve, reject));
|
|
123
|
+
}),
|
|
124
|
+
geteuid: () => process.geteuid?.() ?? -1,
|
|
125
|
+
write: (fd, data) => new Promise((resolve, reject) => {
|
|
126
|
+
write(fd, data, 0, data.byteLength, null, settle(resolve, reject));
|
|
127
|
+
}),
|
|
128
|
+
fdatasync: (fd) => new Promise((resolve, reject) => {
|
|
129
|
+
fdatasync(fd, (error) => {
|
|
130
|
+
if (error)
|
|
131
|
+
reject(error);
|
|
132
|
+
else
|
|
133
|
+
resolve();
|
|
134
|
+
});
|
|
135
|
+
}),
|
|
136
|
+
close: (fd) => new Promise((resolve, reject) => {
|
|
137
|
+
close(fd, (error) => {
|
|
138
|
+
if (error)
|
|
139
|
+
reject(error);
|
|
140
|
+
else
|
|
141
|
+
resolve();
|
|
142
|
+
});
|
|
143
|
+
}),
|
|
144
|
+
});
|
|
145
|
+
/** Linux O_CLOEXEC; Node does not export it, and libuv also sets it on open. */
|
|
146
|
+
const O_CLOEXEC = 0o2000000;
|
|
147
|
+
/**
|
|
148
|
+
* BR-239 flags. O_NONBLOCK makes an open of a FIFO without reader fail instead
|
|
149
|
+
* of blocking startup; it has no effect on a regular file.
|
|
150
|
+
*/
|
|
151
|
+
export const AUDIT_OPEN_FLAGS = constants.O_WRONLY |
|
|
152
|
+
constants.O_APPEND |
|
|
153
|
+
constants.O_CREAT |
|
|
154
|
+
constants.O_NOFOLLOW |
|
|
155
|
+
constants.O_NONBLOCK |
|
|
156
|
+
O_CLOEXEC;
|
|
157
|
+
/** BR-239–BR-242, BR-259: the private append-only file sink. */
|
|
158
|
+
export async function openFileSink(path, options = {}) {
|
|
159
|
+
const port = options.port ?? productionFilePort;
|
|
160
|
+
const timeoutMs = options.timeoutMs ?? AUDIT_APPEND_TIMEOUT_MS;
|
|
161
|
+
let fd;
|
|
162
|
+
try {
|
|
163
|
+
fd = await port.open(path, AUDIT_OPEN_FLAGS, 0o600);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
throw new AuditStartupError();
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const stat = await port.fstat(fd);
|
|
170
|
+
if (!stat.isFile() ||
|
|
171
|
+
stat.uid !== port.geteuid() ||
|
|
172
|
+
(stat.mode & 0o077) !== 0)
|
|
173
|
+
throw new AuditStartupError();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
await port.close(fd).catch(() => undefined);
|
|
177
|
+
throw new AuditStartupError();
|
|
178
|
+
}
|
|
179
|
+
return serialSink((line) => new Promise((resolve, reject) => {
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
reject(new AuditUnavailable());
|
|
182
|
+
}, timeoutMs);
|
|
183
|
+
const data = Buffer.from(line);
|
|
184
|
+
void (async () => {
|
|
185
|
+
const written = await port.write(fd, data);
|
|
186
|
+
if (written !== data.byteLength)
|
|
187
|
+
throw new AuditUnavailable();
|
|
188
|
+
await port.fdatasync(fd);
|
|
189
|
+
})().then(() => {
|
|
190
|
+
clearTimeout(timer);
|
|
191
|
+
resolve();
|
|
192
|
+
}, (error) => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
reject(error instanceof Error ? error : new AuditUnavailable());
|
|
195
|
+
});
|
|
196
|
+
}), () => port.close(fd).catch(() => undefined));
|
|
197
|
+
}
|
|
198
|
+
/** BR-238/BR-240/BR-241: records on file descriptor 2, resolved by the write callback. */
|
|
199
|
+
export function createStderrSink(stream) {
|
|
200
|
+
let failed = false;
|
|
201
|
+
const sink = serialSink((line) => new Promise((resolve, reject) => {
|
|
202
|
+
if (failed) {
|
|
203
|
+
reject(new AuditUnavailable());
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
stream.write(line, (error) => {
|
|
207
|
+
if (error || failed)
|
|
208
|
+
reject(new AuditUnavailable());
|
|
209
|
+
else
|
|
210
|
+
resolve();
|
|
211
|
+
});
|
|
212
|
+
}), () => Promise.resolve());
|
|
213
|
+
stream.on?.("error", () => {
|
|
214
|
+
failed = true;
|
|
215
|
+
sink.latch();
|
|
216
|
+
});
|
|
217
|
+
return sink;
|
|
218
|
+
}
|
|
219
|
+
/** BR-238: `stderr` or an absolute file path (F-002). */
|
|
220
|
+
export async function openAuditSink(auditLog, options) {
|
|
221
|
+
if (auditLog === "stderr")
|
|
222
|
+
return createStderrSink(options.stderr);
|
|
223
|
+
return openFileSink(auditLog, options.port ? { port: options.port } : {});
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=sink.js.map
|