n8n-mcp-community 0.1.0 → 0.1.2
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/CHANGELOG.md +56 -1
- package/CONTRIBUTING.md +12 -4
- package/README.md +71 -23
- package/SECURITY.md +6 -7
- package/dist/introspect/contracts.js +42 -17
- package/dist/security/redaction.js +93 -17
- package/dist/tools/common.js +0 -29
- package/dist/tools/credentials.js +85 -31
- package/dist/tools/definition.js +67 -25
- package/dist/tools/executions.js +39 -20
- package/dist/tools/output-contracts.js +490 -0
- package/dist/tools/response-contracts.js +238 -0
- package/dist/tools/schemas.js +15 -4
- package/dist/tools/tags.js +20 -12
- package/dist/tools/users.js +39 -18
- package/dist/tools/utilities.js +89 -24
- package/dist/tools/workflows.js +372 -64
- package/dist/version.js +1 -1
- package/docs/architecture.md +10 -2
- package/docs/assets/demo.gif +0 -0
- package/docs/clients.md +19 -19
- package/docs/compatibility.md +18 -16
- package/docs/demo-transcript.md +5 -4
- package/docs/faq.md +5 -4
- package/docs/getting-started.md +6 -6
- package/docs/installation.md +36 -26
- package/docs/provenance.md +4 -2
- package/docs/security-model.md +21 -6
- package/docs/tools.md +42 -17
- package/docs/troubleshooting.md +4 -3
- package/package.json +11 -3
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,59 @@ All notable changes to this project will be documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.2] - Frozen release candidate
|
|
10
|
+
|
|
11
|
+
Version 0.1.2 is a frozen release candidate. Availability is established only
|
|
12
|
+
by matching npm provenance, GitHub Release assets and signatures, and MCP
|
|
13
|
+
Registry readbacks; this immutable entry is not rewritten after publication.
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- The transitive `fast-uri` dependency is updated past
|
|
18
|
+
GHSA-v2hh-gcrm-f6hx, and `@hono/node-server` is overridden to the patched
|
|
19
|
+
2.x line for GHSA-frvp-7c67-39w9. The affected HTTP adapter is unused by
|
|
20
|
+
this stdio-only server. The project and bundled MCPB trees audit clean, but
|
|
21
|
+
npm does not inherit dependency-package overrides: a disposable downstream
|
|
22
|
+
consumer resolves the reviewed 1.19.15 backport through MCP SDK 1.29.0.
|
|
23
|
+
The gate accepts only the exact known advisory or a fully clean advisory
|
|
24
|
+
readback while registry metadata converges, and closure still requires a clean
|
|
25
|
+
no-override audit of the published 0.1.2 consumer.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- All 44 MCP-visible tool definitions now state their specific purpose, safe
|
|
30
|
+
usage boundary, named sibling alternatives, behavior, and return semantics.
|
|
31
|
+
- Every top-level input and output field now publishes a meaningful JSON Schema
|
|
32
|
+
description, including the dedicated `n8n_introspect` result contract.
|
|
33
|
+
- Every generic tool now publishes its own required `data` result contract, and
|
|
34
|
+
an exact normalized `tools/list` metadata fingerprint prevents semantic drift.
|
|
35
|
+
- Workflow create/update now express the two-number node position as a bounded
|
|
36
|
+
homogeneous JSON Schema array. Runtime validation is unchanged, while Codex
|
|
37
|
+
can expose both tools instead of rejecting the tuple-form `items` array.
|
|
38
|
+
- Fork pull requests use a complete keyless contributor gate. Only trusted
|
|
39
|
+
maintainer pushes can authenticate and approve an exact artifact baseline.
|
|
40
|
+
- Installation documentation no longer treats a source-tree version as proof
|
|
41
|
+
that npm, GitHub, signatures, provenance, or Registry publication occurred.
|
|
42
|
+
- Private Glama review captures are excluded from the public Git boundary.
|
|
43
|
+
|
|
44
|
+
## [0.1.1] - 2026-07-21
|
|
45
|
+
|
|
46
|
+
### Security
|
|
47
|
+
|
|
48
|
+
- npm publication is now exclusively GitHub OIDC trusted publishing: the
|
|
49
|
+
v0.1.0 first-publish bootstrap token binding was removed from the release
|
|
50
|
+
workflow, and the release policy fails closed on any npm token reference.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- Documentation now describes the published state: the README leads with the
|
|
55
|
+
pinned exact-version `npx` quick start, states up front that the safety
|
|
56
|
+
modes are progressive rather than restrictive, and the installation,
|
|
57
|
+
client, compatibility, and FAQ guides replace pre-release "planned"
|
|
58
|
+
wording with the released routes.
|
|
59
|
+
- Release-asset links use the stable latest-release URL so documentation
|
|
60
|
+
links remain valid across versions.
|
|
61
|
+
|
|
9
62
|
## [0.1.0] - 2026-07-20
|
|
10
63
|
|
|
11
64
|
### Added
|
|
@@ -112,5 +165,7 @@ All notable changes to this project will be documented here. The format follows
|
|
|
112
165
|
credentials, keys, and generated release artifacts cannot survive in that
|
|
113
166
|
publishable history; authenticated public release receipts remain supported.
|
|
114
167
|
|
|
115
|
-
[Unreleased]:
|
|
168
|
+
[Unreleased]: #unreleased
|
|
169
|
+
[0.1.2]: #012---frozen-release-candidate
|
|
170
|
+
[0.1.1]: https://github.com/drzamarian/n8n-mcp-community/releases/tag/v0.1.1
|
|
116
171
|
[0.1.0]: https://github.com/drzamarian/n8n-mcp-community/releases/tag/v0.1.0
|
package/CONTRIBUTING.md
CHANGED
|
@@ -21,14 +21,22 @@ Node.js 22 and 24 are the supported development lines.
|
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm ci
|
|
24
|
-
npm run
|
|
24
|
+
npm run verify:contributor
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The
|
|
28
|
-
TypeScript, the full automated test suite,
|
|
29
|
-
production
|
|
27
|
+
The keyless contributor gate covers formatting, dependency licenses and notices,
|
|
28
|
+
strict TypeScript, the full automated test suite, real stdio inventories, package
|
|
29
|
+
and MCPB construction, a disposable downstream-consumer audit, and the production
|
|
30
|
+
build. It deliberately does not authenticate or approve the maintainer's exact
|
|
31
|
+
artifact baseline.
|
|
30
32
|
Run `npm run sbom > sbom.cdx.json` when reviewing dependency changes.
|
|
31
33
|
|
|
34
|
+
Pull requests from forks run only this keyless gate; GitHub does not expose the
|
|
35
|
+
operator-held artifact-approval secret to fork workflows. After review, the
|
|
36
|
+
maintainer regenerates the exact baseline, approves it interactively, and runs
|
|
37
|
+
`npm run verify`. Release and publication workflows accept only that keyed path;
|
|
38
|
+
the contributor gate cannot authorize an artifact, tag, or release.
|
|
39
|
+
|
|
32
40
|
## Branch and commit conventions
|
|
33
41
|
|
|
34
42
|
- Create a focused branch from `dev`: `feat/<topic>`, `fix/<topic>`,
|
package/README.md
CHANGED
|
@@ -2,20 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
**A security-focused Model Context Protocol server for self-hosted n8n Community Edition.**
|
|
4
4
|
|
|
5
|
-
Connect an MCP client to
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
Connect an MCP client to a **bounded 44-tool n8n Community Edition management surface**: create, edit,
|
|
6
|
+
activate, and delete workflows, update individual nodes surgically, and manage
|
|
7
|
+
executions, credentials, tags, users, diagnostics, and instance metadata — 44
|
|
8
|
+
carefully bounded tools in total. Safety here is **progressive, not
|
|
9
|
+
restrictive**: the server defaults to read-only, `write` unlocks authoring,
|
|
10
|
+
and `unsafe` unlocks every tool with an exact per-call confirmation phrase for
|
|
11
|
+
each destructive operation. It starts offline, uses the supported n8n Public
|
|
12
|
+
API, and never sends workflow data to an external AI provider.
|
|
13
|
+
|
|
14
|
+
> **Release evidence:** install only a version that appears consistently on
|
|
15
|
+
> [npm](https://www.npmjs.com/package/n8n-mcp-community), the
|
|
16
|
+
> [latest GitHub release](https://github.com/drzamarian/n8n-mcp-community/releases/latest),
|
|
17
|
+
> and the MCP Registry entry `io.github.drzamarian/n8n-mcp-community`. A version
|
|
18
|
+
> in the source tree remains a candidate until those external readbacks agree.
|
|
19
|
+
> See [Installation](docs/installation.md).
|
|
13
20
|
|
|
14
21
|
[Read the synthetic terminal demo transcript](docs/demo-transcript.md). It shows
|
|
15
|
-
the
|
|
22
|
+
the exact-version startup, 44-tool inventory, and local Introspect
|
|
16
23
|
diagnostics using only synthetic identifiers and documented output shapes.
|
|
17
24
|
|
|
18
|
-
[](docs/demo-transcript.md)
|
|
19
26
|
|
|
20
27
|
## Why this project
|
|
21
28
|
|
|
@@ -40,31 +47,71 @@ diagnostics using only synthetic identifiers and documented output shapes.
|
|
|
40
47
|
The current source candidate has been verified with:
|
|
41
48
|
|
|
42
49
|
- exactly **44 tools**, **5 resources**, and **4 prompts** over real stdio;
|
|
43
|
-
- **
|
|
50
|
+
- **291 passing tests** and the complete verification gate on Node.js 22.23.1
|
|
44
51
|
and 24.18.0;
|
|
45
|
-
- zero findings from Gitleaks, Semgrep, Trivy, and both
|
|
46
|
-
`npm audit` runs; all three source scanners are reproduced in
|
|
47
|
-
immutable scanner/action identities;
|
|
52
|
+
- zero findings from Gitleaks, Semgrep, Trivy, and both project-root
|
|
53
|
+
production/full `npm audit` runs; all three source scanners are reproduced in
|
|
54
|
+
CI, with immutable scanner/action identities;
|
|
55
|
+
- a separate no-override consumer install that resolves the reviewed
|
|
56
|
+
`@hono/node-server@1.19.15` backport, accepts only an exact
|
|
57
|
+
GHSA-frvp-7c67-39w9 advisory or a fully clean advisory readback while registry
|
|
58
|
+
metadata converges, and proves the stdio runtime and exact 44/5/4 inventory;
|
|
48
59
|
- a reproducible dependency-license gate covering 224 installed package paths;
|
|
49
60
|
- bounded same-origin HTTP contracts and zero-request policy-denial tests;
|
|
50
61
|
- all 44 compiled tool lifecycles on disposable n8n Community 2.30.5 and 2.30.7
|
|
51
62
|
instances with egress isolation, revoked keys, cleanup, and zero residue; and
|
|
52
|
-
- a
|
|
53
|
-
|
|
63
|
+
- a reviewed offline npm-tarball installation under the project's committed
|
|
64
|
+
dependency policy, plus a byte-reproducible unsigned MCPB with the same
|
|
65
|
+
compiled runtime and complete production dependency notices.
|
|
54
66
|
|
|
55
67
|
Publication proceeds only through the reviewed release procedure: the
|
|
56
68
|
repository-pinned MCPB signing identity, a human artifact-baseline receipt,
|
|
57
69
|
tag-scoped protected-environment approval, and an externally signed MCPB
|
|
58
70
|
handoff verified byte for byte against the reviewed unsigned candidate.
|
|
59
71
|
|
|
60
|
-
## Quick start
|
|
72
|
+
## Quick start
|
|
61
73
|
|
|
62
74
|
Requirements: Node.js 22 or 24, npm, a self-hosted n8n Community Edition
|
|
63
75
|
instance, and an n8n Public API key with only the permissions you need.
|
|
64
76
|
|
|
77
|
+
First identify a version whose npm provenance and GitHub release assets you have
|
|
78
|
+
verified, then replace `<VERIFIED_VERSION>` below. Keep the pin exact:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"mcpServers": {
|
|
83
|
+
"n8n-community": {
|
|
84
|
+
"command": "npx",
|
|
85
|
+
"args": ["--yes", "n8n-mcp-community@<VERIFIED_VERSION>"],
|
|
86
|
+
"env": {
|
|
87
|
+
"N8N_API_URL": "https://n8n.example.com",
|
|
88
|
+
"N8N_API_KEY": "replace-with-a-dedicated-api-key",
|
|
89
|
+
"N8N_MCP_MODE": "read-only"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`read-only` is the recommended starting point, not a capability ceiling: the
|
|
97
|
+
same 44-tool surface includes full workflow authoring. Set
|
|
98
|
+
`N8N_MCP_MODE=write` to create and edit workflows, nodes, credentials, and
|
|
99
|
+
tags, or `N8N_MCP_MODE=unsafe` to enable every tool — activation, deletion,
|
|
100
|
+
retries, and the other destructive operations, each still guarded by an exact
|
|
101
|
+
per-call confirmation phrase. See [Safety modes](#safety-modes).
|
|
102
|
+
|
|
103
|
+
Restart the MCP client and confirm exactly 44 tools, 5 resources, and 4
|
|
104
|
+
prompts. Compatible desktop clients can instead install the signed MCPB from
|
|
105
|
+
the [latest verified release](https://github.com/drzamarian/n8n-mcp-community/releases/latest);
|
|
106
|
+
verify its checksum against the release `SHA256SUMS` first.
|
|
107
|
+
|
|
108
|
+
## Quick start from source
|
|
109
|
+
|
|
110
|
+
Requirements: Node.js 22 or 24, npm, and a Git checkout of this repository.
|
|
111
|
+
|
|
65
112
|
```bash
|
|
66
113
|
npm ci
|
|
67
|
-
npm run
|
|
114
|
+
npm run verify:contributor
|
|
68
115
|
```
|
|
69
116
|
|
|
70
117
|
Configure your MCP client to run the compiled stdio entry point:
|
|
@@ -89,7 +136,7 @@ Restart the MCP client and list tools. The server can initialize and expose its
|
|
|
89
136
|
inventory without n8n credentials; connected tools validate the URL and API key
|
|
90
137
|
only when called.
|
|
91
138
|
|
|
92
|
-
|
|
139
|
+
Verified releases provide both a signed MCPB for compatible clients and
|
|
93
140
|
exact-version `npx` configuration for portability. `@latest`, global installs,
|
|
94
141
|
and `curl | shell` are not reproducible defaults. See
|
|
95
142
|
[Installation](docs/installation.md) for the release policy and client-specific
|
|
@@ -241,12 +288,13 @@ Read the complete [security model](docs/security-model.md) and use
|
|
|
241
288
|
|
|
242
289
|
```bash
|
|
243
290
|
npm ci
|
|
244
|
-
npm run
|
|
291
|
+
npm run verify:contributor
|
|
245
292
|
npm run sbom > sbom.cdx.json
|
|
246
293
|
```
|
|
247
294
|
|
|
248
|
-
`npm run
|
|
249
|
-
|
|
295
|
+
`npm run verify:contributor` runs the complete keyless contributor gate,
|
|
296
|
+
including formatting, dependency licenses/notices, strict TypeScript, all
|
|
297
|
+
tests, the compiled runtime, and documentation inventory parity.
|
|
250
298
|
See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a change.
|
|
251
299
|
|
|
252
300
|
## Scope and non-affiliation
|
|
@@ -257,7 +305,7 @@ API. The project does not redistribute the `n8n-nodes-base` catalog and does not
|
|
|
257
305
|
use browser cookies, interactive session routes, or runtime package downloads to
|
|
258
306
|
construct one.
|
|
259
307
|
|
|
260
|
-
The v0.1.
|
|
308
|
+
The v0.1.2 candidate surface intentionally excludes arbitrary workflow execution,
|
|
261
309
|
credential/workflow transfer, folders, data tables, beta evaluation endpoints,
|
|
262
310
|
and execution annotations. See the [roadmap](ROADMAP.md) for the annotation
|
|
263
311
|
proposal retained outside the release target.
|
package/SECURITY.md
CHANGED
|
@@ -6,16 +6,15 @@ before public disclosure.
|
|
|
6
6
|
|
|
7
7
|
## Supported versions
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
asked to upgrade when a safe backport is impractical.
|
|
9
|
+
The latest maintained minor line — currently 0.1.x — receives security fixes.
|
|
10
|
+
Older lines may be asked to upgrade when a safe backport is impractical.
|
|
12
11
|
|
|
13
12
|
## Report a vulnerability
|
|
14
13
|
|
|
15
|
-
Use GitHub's **Security → Report a vulnerability** form
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
Use GitHub's **Security → Report a vulnerability** form; private vulnerability
|
|
15
|
+
reporting is enabled and verified for this repository. Do not open a public
|
|
16
|
+
issue with exploit details, credentials, workflow payloads, or identifying
|
|
17
|
+
instance data.
|
|
19
18
|
|
|
20
19
|
If the private form is unavailable, open a public issue containing only the
|
|
21
20
|
sentence “Private security contact requested.” Do not include technical details
|
|
@@ -219,17 +219,28 @@ const DurationMetricsSchema = z.object({
|
|
|
219
219
|
p99Ms: z.number().nonnegative().nullable(),
|
|
220
220
|
});
|
|
221
221
|
export const IntrospectResultSchema = z.object({
|
|
222
|
-
schemaVersion: z
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
222
|
+
schemaVersion: z
|
|
223
|
+
.literal(INTROSPECT_SCHEMA_VERSION)
|
|
224
|
+
.describe("Version of the stable structured introspect result contract."),
|
|
225
|
+
engineVersion: z
|
|
226
|
+
.literal(INTROSPECT_ENGINE_VERSION)
|
|
227
|
+
.describe("Version of the deterministic local analysis engine that produced this result."),
|
|
228
|
+
status: z
|
|
229
|
+
.enum(["complete", "partial"])
|
|
230
|
+
.describe("Whether all requested bounded evidence was collected and analyzed."),
|
|
231
|
+
profile: z
|
|
232
|
+
.enum(["quick", "deep"])
|
|
233
|
+
.describe("Effective quick or deep analysis profile used for this result."),
|
|
234
|
+
workflow: z
|
|
235
|
+
.object({
|
|
227
236
|
id: z.string(),
|
|
228
237
|
label: z.string().optional(),
|
|
229
238
|
active: z.boolean(),
|
|
230
239
|
nodeCount: z.number().int().nonnegative(),
|
|
231
|
-
})
|
|
232
|
-
|
|
240
|
+
})
|
|
241
|
+
.describe("Sanitized identity and basic structure of the inspected workflow."),
|
|
242
|
+
sample: z
|
|
243
|
+
.object({
|
|
233
244
|
lookbackHours: z.number().int().positive(),
|
|
234
245
|
maxExecutions: z.number().int().positive(),
|
|
235
246
|
metadataExecutions: z.number().int().nonnegative(),
|
|
@@ -249,8 +260,10 @@ export const IntrospectResultSchema = z.object({
|
|
|
249
260
|
]),
|
|
250
261
|
oldestStartedAt: z.string().nullable(),
|
|
251
262
|
newestStartedAt: z.string().nullable(),
|
|
252
|
-
})
|
|
253
|
-
|
|
263
|
+
})
|
|
264
|
+
.describe("Exact execution-history scope, ordering confidence, requests, and byte budget used."),
|
|
265
|
+
summary: z
|
|
266
|
+
.object({
|
|
254
267
|
findingCounts: z.object({
|
|
255
268
|
critical: z.number().int().nonnegative(),
|
|
256
269
|
high: z.number().int().nonnegative(),
|
|
@@ -271,8 +284,10 @@ export const IntrospectResultSchema = z.object({
|
|
|
271
284
|
omittedCount: z.number().int().nonnegative(),
|
|
272
285
|
truncated: z.boolean(),
|
|
273
286
|
}),
|
|
274
|
-
})
|
|
275
|
-
|
|
287
|
+
})
|
|
288
|
+
.describe("Counts of findings, rule outcomes, and any details omitted by output bounds."),
|
|
289
|
+
metrics: z
|
|
290
|
+
.object({
|
|
276
291
|
statusCounts: StatusCountsSchema,
|
|
277
292
|
successRate: z.number().min(0).max(1).nullable(),
|
|
278
293
|
duration: DurationMetricsSchema,
|
|
@@ -296,14 +311,24 @@ export const IntrospectResultSchema = z.object({
|
|
|
296
311
|
sampledErrors: z.number().int().nonnegative(),
|
|
297
312
|
}))
|
|
298
313
|
.max(100),
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
|
|
314
|
+
})
|
|
315
|
+
.describe("Factual status, duration, node-timing, and value-free error-cluster measurements."),
|
|
316
|
+
findings: z
|
|
317
|
+
.array(FindingSchema)
|
|
318
|
+
.max(1_000)
|
|
319
|
+
.describe("Ordered deterministic findings retained within the configured output limit."),
|
|
320
|
+
ruleCoverage: z
|
|
321
|
+
.array(RuleCoverageSchema)
|
|
322
|
+
.max(100)
|
|
323
|
+
.describe("Per-rule outcome and completeness evidence, including omitted finding counts."),
|
|
302
324
|
limitations: z
|
|
303
325
|
.array(z.object({ code: z.enum(INTROSPECT_LIMITATION_CODES), message: z.string() }))
|
|
304
|
-
.max(100)
|
|
305
|
-
|
|
326
|
+
.max(100)
|
|
327
|
+
.describe("Collection or analysis limitations that constrain interpretation of this result."),
|
|
328
|
+
guidance: z
|
|
329
|
+
.object({
|
|
306
330
|
instanceSecurityTool: z.literal("n8n_audit_generate"),
|
|
307
331
|
message: z.string(),
|
|
308
|
-
})
|
|
332
|
+
})
|
|
333
|
+
.describe("Boundary guidance pointing to the separate instance-level n8n security audit tool."),
|
|
309
334
|
});
|
|
@@ -38,6 +38,8 @@ const VALUE_REDACTIONS = [
|
|
|
38
38
|
[/\bpix[\s:-]*[\w.@-]{8,}/gi, "[PIX]"],
|
|
39
39
|
[/\b(?:[A-Z0-9]+_)*(?:SECRET|TOKEN|API_KEY)(?:_[A-Z0-9]+)+\b/gi, "[SECRET]"],
|
|
40
40
|
];
|
|
41
|
+
const OPAQUE_PATH_CHARACTERS = /^[A-Za-z0-9._~+/=-]+$/;
|
|
42
|
+
const OPAQUE_HEXADECIMAL_PATH = /^[A-Fa-f0-9]{24,64}$/;
|
|
41
43
|
const INJECTION_PATTERNS = [
|
|
42
44
|
[
|
|
43
45
|
/\b(?:ignore|disregard|forget|override)\s+(?:all\s+)?(?:previous|prior|above)\s+instructions?\b/gi,
|
|
@@ -46,6 +48,12 @@ const INJECTION_PATTERNS = [
|
|
|
46
48
|
[/\b(?:system|assistant|developer)\s*:\s/gi, "[FILTERED-ROLE]: "],
|
|
47
49
|
[/<\/?(?:system|assistant|developer|prompt|instructions?)>/gi, "[FILTERED-TAG]"],
|
|
48
50
|
];
|
|
51
|
+
export class OutputLimitError extends Error {
|
|
52
|
+
constructor() {
|
|
53
|
+
super("The sanitized result exceeds the output limit. Use narrower filters or pagination.");
|
|
54
|
+
this.name = "OutputLimitError";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
49
57
|
function compareStrings(left, right) {
|
|
50
58
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
51
59
|
}
|
|
@@ -77,6 +85,24 @@ function redactUrlCredential(value) {
|
|
|
77
85
|
// quantifier is bounded so a long delimiter-free run cannot force quadratic backtracking.
|
|
78
86
|
return value.replace(/([A-Za-z][A-Za-z0-9+.-]{0,31}:\/\/)[^/?#\s]+@/g, "$1[REDACTED]@");
|
|
79
87
|
}
|
|
88
|
+
function looksLikeOpaquePathSegment(value) {
|
|
89
|
+
if (value.length < 24 || !OPAQUE_PATH_CHARACTERS.test(value))
|
|
90
|
+
return false;
|
|
91
|
+
// Encoding-aware detection closes the entropy blind spot for fixed-width hexadecimal IDs:
|
|
92
|
+
// repeated hex digits can carry 96+ bits while scoring below the generic Shannon threshold.
|
|
93
|
+
if (OPAQUE_HEXADECIMAL_PATH.test(value))
|
|
94
|
+
return true;
|
|
95
|
+
const frequencies = new Map();
|
|
96
|
+
for (const character of value) {
|
|
97
|
+
frequencies.set(character, (frequencies.get(character) ?? 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
let entropy = 0;
|
|
100
|
+
for (const count of frequencies.values()) {
|
|
101
|
+
const probability = count / value.length;
|
|
102
|
+
entropy -= probability * Math.log2(probability);
|
|
103
|
+
}
|
|
104
|
+
return entropy >= (value.length >= 32 ? 3.5 : 4);
|
|
105
|
+
}
|
|
80
106
|
function sanitizeString(value, structuralKey) {
|
|
81
107
|
const boundedInput = value.length > 131_072 ? value.slice(0, 131_072) : value;
|
|
82
108
|
const normalizedInput = boundedInput.normalize("NFKC");
|
|
@@ -150,10 +176,11 @@ function sanitizeString(value, structuralKey) {
|
|
|
150
176
|
}
|
|
151
177
|
return { value: output, redacted };
|
|
152
178
|
}
|
|
153
|
-
function sanitizeValue(value, state, depth, structuralKey, preserveChildKeys = false, sensitiveDescriptor = false) {
|
|
179
|
+
function sanitizeValue(value, state, depth, structuralKey, preserveChildKeys = false, sensitiveDescriptor = false, treatSensitiveKeyAsStructural = false) {
|
|
154
180
|
state.nodes += 1;
|
|
155
181
|
if (state.nodes > 20_000 || depth > 20) {
|
|
156
182
|
state.redacted = true;
|
|
183
|
+
state.structurallyReduced = true;
|
|
157
184
|
return "[TRUNCATED]";
|
|
158
185
|
}
|
|
159
186
|
if (typeof value === "string") {
|
|
@@ -165,18 +192,22 @@ function sanitizeValue(value, state, depth, structuralKey, preserveChildKeys = f
|
|
|
165
192
|
return value;
|
|
166
193
|
if (Array.isArray(value)) {
|
|
167
194
|
const limit = Math.min(value.length, 1_000);
|
|
168
|
-
if (limit < value.length)
|
|
195
|
+
if (limit < value.length) {
|
|
169
196
|
state.redacted = true;
|
|
197
|
+
state.structurallyReduced = true;
|
|
198
|
+
}
|
|
170
199
|
return value
|
|
171
200
|
.slice(0, limit)
|
|
172
201
|
.map((item) => sanitizeValue(item, state, depth + 1, structuralKey, false, sensitiveDescriptor));
|
|
173
202
|
}
|
|
174
203
|
if (typeof value === "bigint") {
|
|
175
204
|
state.redacted = true;
|
|
205
|
+
state.structurallyReduced = true;
|
|
176
206
|
return value.toString();
|
|
177
207
|
}
|
|
178
208
|
if (typeof value !== "object") {
|
|
179
209
|
state.redacted = true;
|
|
210
|
+
state.structurallyReduced = true;
|
|
180
211
|
return "[UNSUPPORTED]";
|
|
181
212
|
}
|
|
182
213
|
const prototype = Reflect.getPrototypeOf(value);
|
|
@@ -185,6 +216,7 @@ function sanitizeValue(value, state, depth, structuralKey, preserveChildKeys = f
|
|
|
185
216
|
value instanceof Set ||
|
|
186
217
|
(prototype !== Object.prototype && prototype !== null)) {
|
|
187
218
|
state.redacted = true;
|
|
219
|
+
state.structurallyReduced = true;
|
|
188
220
|
return "[UNSUPPORTED_OBJECT]";
|
|
189
221
|
}
|
|
190
222
|
const output = Object.create(null);
|
|
@@ -192,50 +224,75 @@ function sanitizeValue(value, state, depth, structuralKey, preserveChildKeys = f
|
|
|
192
224
|
const sensitiveSibling = entries.some(([key, candidate]) => (key === "key" || key === "name") &&
|
|
193
225
|
typeof candidate === "string" &&
|
|
194
226
|
SENSITIVE_KEY.test(candidate));
|
|
195
|
-
|
|
227
|
+
const originalKeys = new Set(entries.map(([key]) => key));
|
|
228
|
+
for (const [entryIndex, [key, child]] of entries.slice(0, 1_000).entries()) {
|
|
196
229
|
if (PROTOTYPE_KEY.test(key)) {
|
|
197
230
|
state.redacted = true;
|
|
198
231
|
continue;
|
|
199
232
|
}
|
|
200
|
-
|
|
201
|
-
|
|
233
|
+
const sanitizedKey = sanitizeString(key);
|
|
234
|
+
let outputKey = key;
|
|
235
|
+
if (sanitizedKey.redacted) {
|
|
236
|
+
let placeholderIndex = entryIndex + 1;
|
|
237
|
+
do {
|
|
238
|
+
outputKey = `[REDACTED_KEY_${placeholderIndex}]`;
|
|
239
|
+
placeholderIndex += 1;
|
|
240
|
+
} while (originalKeys.has(outputKey) || Object.hasOwn(output, outputKey));
|
|
241
|
+
state.redacted = true;
|
|
242
|
+
state.structurallyReduced = true;
|
|
243
|
+
}
|
|
244
|
+
if (!treatSensitiveKeyAsStructural &&
|
|
245
|
+
!preserveChildKeys &&
|
|
246
|
+
SENSITIVE_KEY.test(key) &&
|
|
247
|
+
!STRUCTURAL_IDENTIFIER_KEY.test(key)) {
|
|
248
|
+
output[outputKey] = "[REDACTED]";
|
|
202
249
|
state.redacted = true;
|
|
203
250
|
continue;
|
|
204
251
|
}
|
|
205
252
|
if (sensitiveDescriptor && SENSITIVE_DESCRIPTOR_VALUE_KEY.test(key)) {
|
|
206
|
-
output[
|
|
253
|
+
output[outputKey] = "[REDACTED]";
|
|
207
254
|
state.redacted = true;
|
|
208
255
|
continue;
|
|
209
256
|
}
|
|
210
257
|
if (sensitiveDescriptor && child !== null && typeof child === "object") {
|
|
211
|
-
output[
|
|
258
|
+
output[outputKey] = "[REDACTED]";
|
|
212
259
|
state.redacted = true;
|
|
213
260
|
continue;
|
|
214
261
|
}
|
|
215
262
|
if (key === "value" && sensitiveSibling) {
|
|
216
|
-
output[
|
|
263
|
+
output[outputKey] = "[REDACTED]";
|
|
217
264
|
state.redacted = true;
|
|
218
265
|
continue;
|
|
219
266
|
}
|
|
220
|
-
if (preserveChildKeys &&
|
|
267
|
+
if (preserveChildKeys &&
|
|
268
|
+
(SENSITIVE_KEY.test(key) || (structuralKey === "properties" && sanitizedKey.redacted))) {
|
|
221
269
|
if (structuralKey === "properties" && child !== null && typeof child === "object") {
|
|
222
|
-
output[
|
|
270
|
+
output[outputKey] = sanitizeValue(child, state, depth + 1, key, false, true);
|
|
223
271
|
}
|
|
224
272
|
else {
|
|
225
|
-
output[
|
|
273
|
+
output[outputKey] = "[REDACTED]";
|
|
226
274
|
state.redacted = true;
|
|
227
275
|
}
|
|
228
276
|
continue;
|
|
229
277
|
}
|
|
230
|
-
output[
|
|
278
|
+
output[outputKey] = sanitizeValue(child, state, depth + 1, key, STRUCTURAL_MAP_KEY.test(key), false);
|
|
231
279
|
}
|
|
232
|
-
if (entries.length > 1_000)
|
|
280
|
+
if (entries.length > 1_000) {
|
|
233
281
|
state.redacted = true;
|
|
282
|
+
state.structurallyReduced = true;
|
|
283
|
+
}
|
|
234
284
|
return output;
|
|
235
285
|
}
|
|
236
|
-
export function
|
|
237
|
-
const state = { nodes: 0, redacted: false };
|
|
238
|
-
|
|
286
|
+
export function sanitizeForOutputDetailed(value, options = {}) {
|
|
287
|
+
const state = { nodes: 0, redacted: false, structurallyReduced: false };
|
|
288
|
+
const data = sanitizeValue(value, state, 0, undefined, false, false, options.preserveValidatedRootRecordValues ?? false);
|
|
289
|
+
return {
|
|
290
|
+
output: { data, redacted: state.redacted, untrusted: true },
|
|
291
|
+
structurallyReduced: state.structurallyReduced,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
export function sanitizeForOutput(value, options = {}) {
|
|
295
|
+
return sanitizeForOutputDetailed(value, options).output;
|
|
239
296
|
}
|
|
240
297
|
export function sanitizeLabelForOutput(value, maxLength = 120) {
|
|
241
298
|
const normalized = value.slice(0, 4_096).normalize("NFKC").slice(0, 4_096);
|
|
@@ -244,10 +301,29 @@ export function sanitizeLabelForOutput(value, maxLength = 120) {
|
|
|
244
301
|
? sanitized
|
|
245
302
|
: `${sanitized.slice(0, Math.max(0, maxLength - 1))}\u2026`;
|
|
246
303
|
}
|
|
304
|
+
export function sanitizePathSegmentForOutput(value, maxLength = 64) {
|
|
305
|
+
const normalized = value.slice(0, 4_096).normalize("NFKC").slice(0, 4_096);
|
|
306
|
+
if (SENSITIVE_KEY.test(normalized) ||
|
|
307
|
+
PROTOTYPE_KEY.test(normalized) ||
|
|
308
|
+
looksLikeOpaquePathSegment(normalized)) {
|
|
309
|
+
return { value: "[REDACTED]", redacted: true };
|
|
310
|
+
}
|
|
311
|
+
const sanitized = sanitizeString(normalized);
|
|
312
|
+
const compact = sanitized.value.replace(/\s+/g, " ").trim();
|
|
313
|
+
if (sanitized.redacted || compact.length === 0) {
|
|
314
|
+
return { value: "[REDACTED]", redacted: true };
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
value: compact.length <= maxLength
|
|
318
|
+
? compact
|
|
319
|
+
: `${compact.slice(0, Math.max(0, maxLength - 1))}\u2026`,
|
|
320
|
+
redacted: compact.length > maxLength,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
247
323
|
export function boundedJson(value, maxBytes = 256 * 1024) {
|
|
248
324
|
const text = JSON.stringify(value, null, 2);
|
|
249
325
|
if (Buffer.byteLength(text) > maxBytes) {
|
|
250
|
-
throw new
|
|
326
|
+
throw new OutputLimitError();
|
|
251
327
|
}
|
|
252
328
|
return text;
|
|
253
329
|
}
|
package/dist/tools/common.js
CHANGED
|
@@ -23,32 +23,3 @@ export function requireSafeAscii(value, label, maximumLength = 256) {
|
|
|
23
23
|
}
|
|
24
24
|
return value;
|
|
25
25
|
}
|
|
26
|
-
export function projectMetadata(value) {
|
|
27
|
-
const source = z.record(z.unknown()).parse(value);
|
|
28
|
-
const allowed = [
|
|
29
|
-
"id",
|
|
30
|
-
"name",
|
|
31
|
-
"type",
|
|
32
|
-
"active",
|
|
33
|
-
"isArchived",
|
|
34
|
-
"createdAt",
|
|
35
|
-
"updatedAt",
|
|
36
|
-
"versionId",
|
|
37
|
-
"workflowId",
|
|
38
|
-
"status",
|
|
39
|
-
"startedAt",
|
|
40
|
-
"stoppedAt",
|
|
41
|
-
"mode",
|
|
42
|
-
"finished",
|
|
43
|
-
"retryOf",
|
|
44
|
-
"retrySuccessId",
|
|
45
|
-
"role",
|
|
46
|
-
"disabled",
|
|
47
|
-
];
|
|
48
|
-
const output = {};
|
|
49
|
-
for (const key of allowed) {
|
|
50
|
-
if (Object.hasOwn(source, key))
|
|
51
|
-
output[key] = source[key];
|
|
52
|
-
}
|
|
53
|
-
return output;
|
|
54
|
-
}
|