@sstolz/kdrive-mcp-server 0.0.0-placeholder.0 → 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.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +20 -0
  3. package/README.md +301 -3
  4. package/dist/audit/sink.d.ts +102 -0
  5. package/dist/audit/sink.js +225 -0
  6. package/dist/audit/sink.js.map +1 -0
  7. package/dist/auth/access-token.d.ts +63 -0
  8. package/dist/auth/access-token.js +199 -0
  9. package/dist/auth/access-token.js.map +1 -0
  10. package/dist/auth/proxy/index.d.ts +6 -0
  11. package/dist/auth/proxy/index.js +6 -0
  12. package/dist/auth/proxy/index.js.map +1 -0
  13. package/dist/auth/proxy/keys.d.ts +13 -0
  14. package/dist/auth/proxy/keys.js +42 -0
  15. package/dist/auth/proxy/keys.js.map +1 -0
  16. package/dist/auth/proxy/pages.d.ts +28 -0
  17. package/dist/auth/proxy/pages.js +58 -0
  18. package/dist/auth/proxy/pages.js.map +1 -0
  19. package/dist/auth/proxy/proxy.d.ts +63 -0
  20. package/dist/auth/proxy/proxy.js +1073 -0
  21. package/dist/auth/proxy/proxy.js.map +1 -0
  22. package/dist/auth/proxy/redirect.d.ts +2 -0
  23. package/dist/auth/proxy/redirect.js +34 -0
  24. package/dist/auth/proxy/redirect.js.map +1 -0
  25. package/dist/auth/proxy/store.d.ts +163 -0
  26. package/dist/auth/proxy/store.js +629 -0
  27. package/dist/auth/proxy/store.js.map +1 -0
  28. package/dist/auth/proxy/upstream.d.ts +23 -0
  29. package/dist/auth/proxy/upstream.js +85 -0
  30. package/dist/auth/proxy/upstream.js.map +1 -0
  31. package/dist/auth/verdict.d.ts +56 -0
  32. package/dist/auth/verdict.js +91 -0
  33. package/dist/auth/verdict.js.map +1 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +5 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config-files.d.ts +5 -0
  38. package/dist/config-files.js +66 -0
  39. package/dist/config-files.js.map +1 -0
  40. package/dist/config-schema.d.ts +84 -0
  41. package/dist/config-schema.js +155 -0
  42. package/dist/config-schema.js.map +1 -0
  43. package/dist/config-sources.d.ts +10 -0
  44. package/dist/config-sources.js +202 -0
  45. package/dist/config-sources.js.map +1 -0
  46. package/dist/config-text.d.ts +6 -0
  47. package/dist/config-text.js +26 -0
  48. package/dist/config-text.js.map +1 -0
  49. package/dist/config-values.d.ts +5 -0
  50. package/dist/config-values.js +391 -0
  51. package/dist/config-values.js.map +1 -0
  52. package/dist/config.d.ts +23 -0
  53. package/dist/config.js +186 -0
  54. package/dist/config.js.map +1 -0
  55. package/dist/content.d.ts +60 -0
  56. package/dist/content.js +280 -0
  57. package/dist/content.js.map +1 -0
  58. package/dist/extraction-image.d.ts +27 -0
  59. package/dist/extraction-image.js +204 -0
  60. package/dist/extraction-image.js.map +1 -0
  61. package/dist/extraction-owner.d.ts +4 -0
  62. package/dist/extraction-owner.js +22 -0
  63. package/dist/extraction-owner.js.map +1 -0
  64. package/dist/extraction-pdf-profile.d.ts +7 -0
  65. package/dist/extraction-pdf-profile.js +537 -0
  66. package/dist/extraction-pdf-profile.js.map +1 -0
  67. package/dist/extraction-pdf.d.ts +8 -0
  68. package/dist/extraction-pdf.js +89 -0
  69. package/dist/extraction-pdf.js.map +1 -0
  70. package/dist/extraction-text.d.ts +7 -0
  71. package/dist/extraction-text.js +45 -0
  72. package/dist/extraction-text.js.map +1 -0
  73. package/dist/extraction-values.d.ts +40 -0
  74. package/dist/extraction-values.js +30 -0
  75. package/dist/extraction-values.js.map +1 -0
  76. package/dist/extraction-worker.d.ts +1 -0
  77. package/dist/extraction-worker.js +26 -0
  78. package/dist/extraction-worker.js.map +1 -0
  79. package/dist/extraction.d.ts +7 -0
  80. package/dist/extraction.js +270 -0
  81. package/dist/extraction.js.map +1 -0
  82. package/dist/healthcheck.d.ts +6 -0
  83. package/dist/healthcheck.js +90 -0
  84. package/dist/healthcheck.js.map +1 -0
  85. package/dist/http/adapter.d.ts +24 -0
  86. package/dist/http/adapter.js +139 -0
  87. package/dist/http/adapter.js.map +1 -0
  88. package/dist/http/checks.d.ts +23 -0
  89. package/dist/http/checks.js +90 -0
  90. package/dist/http/checks.js.map +1 -0
  91. package/dist/http/errors.d.ts +8 -0
  92. package/dist/http/errors.js +26 -0
  93. package/dist/http/errors.js.map +1 -0
  94. package/dist/http/handler.d.ts +3 -0
  95. package/dist/http/handler.js +543 -0
  96. package/dist/http/handler.js.map +1 -0
  97. package/dist/http/index.d.ts +4 -0
  98. package/dist/http/index.js +4 -0
  99. package/dist/http/index.js.map +1 -0
  100. package/dist/http/limits.d.ts +37 -0
  101. package/dist/http/limits.js +56 -0
  102. package/dist/http/limits.js.map +1 -0
  103. package/dist/http/responses.d.ts +5 -0
  104. package/dist/http/responses.js +20 -0
  105. package/dist/http/responses.js.map +1 -0
  106. package/dist/http/server.d.ts +18 -0
  107. package/dist/http/server.js +97 -0
  108. package/dist/http/server.js.map +1 -0
  109. package/dist/http/sessions.d.ts +45 -0
  110. package/dist/http/sessions.js +80 -0
  111. package/dist/http/sessions.js.map +1 -0
  112. package/dist/http/types.d.ts +37 -0
  113. package/dist/http/types.js +2 -0
  114. package/dist/http/types.js.map +1 -0
  115. package/dist/kdrive/adapter.d.ts +28 -0
  116. package/dist/kdrive/adapter.js +479 -0
  117. package/dist/kdrive/adapter.js.map +1 -0
  118. package/dist/kdrive/address.d.ts +2 -0
  119. package/dist/kdrive/address.js +88 -0
  120. package/dist/kdrive/address.js.map +1 -0
  121. package/dist/kdrive/client.d.ts +7 -0
  122. package/dist/kdrive/client.js +16 -0
  123. package/dist/kdrive/client.js.map +1 -0
  124. package/dist/kdrive/index.d.ts +3 -0
  125. package/dist/kdrive/index.js +3 -0
  126. package/dist/kdrive/index.js.map +1 -0
  127. package/dist/kdrive/mutators.d.ts +7 -0
  128. package/dist/kdrive/mutators.js +194 -0
  129. package/dist/kdrive/mutators.js.map +1 -0
  130. package/dist/kdrive/normalize.d.ts +91 -0
  131. package/dist/kdrive/normalize.js +285 -0
  132. package/dist/kdrive/normalize.js.map +1 -0
  133. package/dist/kdrive/production.d.ts +9 -0
  134. package/dist/kdrive/production.js +16 -0
  135. package/dist/kdrive/production.js.map +1 -0
  136. package/dist/kdrive/routes.d.ts +38 -0
  137. package/dist/kdrive/routes.js +89 -0
  138. package/dist/kdrive/routes.js.map +1 -0
  139. package/dist/kdrive/transport.d.ts +92 -0
  140. package/dist/kdrive/transport.js +810 -0
  141. package/dist/kdrive/transport.js.map +1 -0
  142. package/dist/main.d.ts +52 -0
  143. package/dist/main.js +220 -0
  144. package/dist/main.js.map +1 -0
  145. package/dist/mutation-gate.d.ts +65 -0
  146. package/dist/mutation-gate.js +439 -0
  147. package/dist/mutation-gate.js.map +1 -0
  148. package/dist/mutation-tools.d.ts +73 -0
  149. package/dist/mutation-tools.js +203 -0
  150. package/dist/mutation-tools.js.map +1 -0
  151. package/dist/navigation.d.ts +45 -0
  152. package/dist/navigation.js +680 -0
  153. package/dist/navigation.js.map +1 -0
  154. package/dist/pagination.d.ts +29 -0
  155. package/dist/pagination.js +192 -0
  156. package/dist/pagination.js.map +1 -0
  157. package/dist/pdf-syntax.d.ts +39 -0
  158. package/dist/pdf-syntax.js +292 -0
  159. package/dist/pdf-syntax.js.map +1 -0
  160. package/dist/policy/glob.d.ts +1 -0
  161. package/dist/policy/glob.js +36 -0
  162. package/dist/policy/glob.js.map +1 -0
  163. package/dist/policy/index.d.ts +88 -0
  164. package/dist/policy/index.js +671 -0
  165. package/dist/policy/index.js.map +1 -0
  166. package/dist/policy/operation.d.ts +17 -0
  167. package/dist/policy/operation.js +86 -0
  168. package/dist/policy/operation.js.map +1 -0
  169. package/dist/policy/received.d.ts +70 -0
  170. package/dist/policy/received.js +412 -0
  171. package/dist/policy/received.js.map +1 -0
  172. package/dist/policy/values.d.ts +46 -0
  173. package/dist/policy/values.js +182 -0
  174. package/dist/policy/values.js.map +1 -0
  175. package/dist/production.d.ts +3 -0
  176. package/dist/production.js +23 -0
  177. package/dist/production.js.map +1 -0
  178. package/dist/read-retry.d.ts +6 -0
  179. package/dist/read-retry.js +45 -0
  180. package/dist/read-retry.js.map +1 -0
  181. package/dist/stdio-transport.d.ts +9 -0
  182. package/dist/stdio-transport.js +289 -0
  183. package/dist/stdio-transport.js.map +1 -0
  184. package/dist/stdio.d.ts +19 -0
  185. package/dist/stdio.js +78 -0
  186. package/dist/stdio.js.map +1 -0
  187. package/dist/version.d.ts +2 -0
  188. package/dist/version.js +3 -0
  189. package/dist/version.js.map +1 -0
  190. package/package.json +60 -3
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,4 +1,302 @@
1
- # @sstolz/kdrive-mcp-server (placeholder)
1
+ # kdrive-mcp-server
2
2
 
3
- This version is a placeholder, not a release.
4
- See https://github.com/sstolz/kdrive-mcp-server and install `@sstolz/kdrive-mcp-server@beta`.
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