n8n-mcp-community 0.1.1 → 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 CHANGED
@@ -6,6 +6,41 @@ 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
+
9
44
  ## [0.1.1] - 2026-07-21
10
45
 
11
46
  ### Security
@@ -130,6 +165,7 @@ All notable changes to this project will be documented here. The format follows
130
165
  credentials, keys, and generated release artifacts cannot survive in that
131
166
  publishable history; authenticated public release receipts remain supported.
132
167
 
133
- [Unreleased]: https://github.com/drzamarian/n8n-mcp-community/compare/v0.1.1...HEAD
168
+ [Unreleased]: #unreleased
169
+ [0.1.2]: #012---frozen-release-candidate
134
170
  [0.1.1]: https://github.com/drzamarian/n8n-mcp-community/releases/tag/v0.1.1
135
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 check
24
+ npm run verify:contributor
25
25
  ```
26
26
 
27
- The complete gate covers formatting, dependency licenses and notices, strict
28
- TypeScript, the full automated test suite, a real stdio inventory check, and the
29
- production build.
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,7 +2,7 @@
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 the **full n8n management surface**: create, edit,
5
+ Connect an MCP client to a **bounded 44-tool n8n Community Edition management surface**: create, edit,
6
6
  activate, and delete workflows, update individual nodes surgically, and manage
7
7
  executions, credentials, tags, users, diagnostics, and instance metadata — 44
8
8
  carefully bounded tools in total. Safety here is **progressive, not
@@ -11,12 +11,12 @@ and `unsafe` unlocks every tool with an exact per-call confirmation phrase for
11
11
  each destructive operation. It starts offline, uses the supported n8n Public
12
12
  API, and never sends workflow data to an external AI provider.
13
13
 
14
- > **v0.1.1 is published:** exact-version npm
15
- > ([`n8n-mcp-community@0.1.1`](https://www.npmjs.com/package/n8n-mcp-community)
16
- > with provenance attestations), a signed MCPB in the
17
- > [v0.1.1 release](https://github.com/drzamarian/n8n-mcp-community/releases/latest),
18
- > and the MCP Registry entry `io.github.drzamarian/n8n-mcp-community`. See
19
- > [Installation](docs/installation.md).
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).
20
20
 
21
21
  [Read the synthetic terminal demo transcript](docs/demo-transcript.md). It shows
22
22
  the exact-version startup, 44-tool inventory, and local Introspect
@@ -47,17 +47,22 @@ diagnostics using only synthetic identifiers and documented output shapes.
47
47
  The current source candidate has been verified with:
48
48
 
49
49
  - exactly **44 tools**, **5 resources**, and **4 prompts** over real stdio;
50
- - **271 passing tests** and the complete verification gate on Node.js 22.23.1
50
+ - **291 passing tests** and the complete verification gate on Node.js 22.23.1
51
51
  and 24.18.0;
52
- - zero findings from Gitleaks, Semgrep, Trivy, and both production/full
53
- `npm audit` runs; all three source scanners are reproduced in CI, with
54
- 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;
55
59
  - a reproducible dependency-license gate covering 224 installed package paths;
56
60
  - bounded same-origin HTTP contracts and zero-request policy-denial tests;
57
61
  - all 44 compiled tool lifecycles on disposable n8n Community 2.30.5 and 2.30.7
58
62
  instances with egress isolation, revoked keys, cleanup, and zero residue; and
59
- - a clean-installed npm tarball plus a byte-reproducible unsigned MCPB with the
60
- same compiled runtime and complete production dependency notices.
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.
61
66
 
62
67
  Publication proceeds only through the reviewed release procedure: the
63
68
  repository-pinned MCPB signing identity, a human artifact-baseline receipt,
@@ -69,14 +74,15 @@ handoff verified byte for byte against the reviewed unsigned candidate.
69
74
  Requirements: Node.js 22 or 24, npm, a self-hosted n8n Community Edition
70
75
  instance, and an n8n Public API key with only the permissions you need.
71
76
 
72
- Configure your MCP client to run the published exact version:
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:
73
79
 
74
80
  ```json
75
81
  {
76
82
  "mcpServers": {
77
83
  "n8n-community": {
78
84
  "command": "npx",
79
- "args": ["--yes", "n8n-mcp-community@0.1.1"],
85
+ "args": ["--yes", "n8n-mcp-community@<VERIFIED_VERSION>"],
80
86
  "env": {
81
87
  "N8N_API_URL": "https://n8n.example.com",
82
88
  "N8N_API_KEY": "replace-with-a-dedicated-api-key",
@@ -96,8 +102,7 @@ per-call confirmation phrase. See [Safety modes](#safety-modes).
96
102
 
97
103
  Restart the MCP client and confirm exactly 44 tools, 5 resources, and 4
98
104
  prompts. Compatible desktop clients can instead install the signed MCPB from
99
- the
100
- [v0.1.1 release](https://github.com/drzamarian/n8n-mcp-community/releases/latest);
105
+ the [latest verified release](https://github.com/drzamarian/n8n-mcp-community/releases/latest);
101
106
  verify its checksum against the release `SHA256SUMS` first.
102
107
 
103
108
  ## Quick start from source
@@ -106,7 +111,7 @@ Requirements: Node.js 22 or 24, npm, and a Git checkout of this repository.
106
111
 
107
112
  ```bash
108
113
  npm ci
109
- npm run build
114
+ npm run verify:contributor
110
115
  ```
111
116
 
112
117
  Configure your MCP client to run the compiled stdio entry point:
@@ -131,7 +136,7 @@ Restart the MCP client and list tools. The server can initialize and expose its
131
136
  inventory without n8n credentials; connected tools validate the URL and API key
132
137
  only when called.
133
138
 
134
- The release provides both a signed MCPB for compatible clients and
139
+ Verified releases provide both a signed MCPB for compatible clients and
135
140
  exact-version `npx` configuration for portability. `@latest`, global installs,
136
141
  and `curl | shell` are not reproducible defaults. See
137
142
  [Installation](docs/installation.md) for the release policy and client-specific
@@ -283,12 +288,13 @@ Read the complete [security model](docs/security-model.md) and use
283
288
 
284
289
  ```bash
285
290
  npm ci
286
- npm run check
291
+ npm run verify:contributor
287
292
  npm run sbom > sbom.cdx.json
288
293
  ```
289
294
 
290
- `npm run check` verifies formatting, dependency licenses/notices, strict
291
- TypeScript, all tests, the compiled runtime, and documentation inventory parity.
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.
292
298
  See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a change.
293
299
 
294
300
  ## Scope and non-affiliation
@@ -299,7 +305,7 @@ API. The project does not redistribute the `n8n-nodes-base` catalog and does not
299
305
  use browser cookies, interactive session routes, or runtime package downloads to
300
306
  construct one.
301
307
 
302
- The v0.1.1 surface intentionally excludes arbitrary workflow execution,
308
+ The v0.1.2 candidate surface intentionally excludes arbitrary workflow execution,
303
309
  credential/workflow transfer, folders, data tables, beta evaluation endpoints,
304
310
  and execution annotations. See the [roadmap](ROADMAP.md) for the annotation
305
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
- No public version is supported until the first signed release. After release,
10
- the latest maintained minor line will receive security fixes. Older lines may be
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. This repository will
16
- not be published until GitHub private vulnerability reporting is enabled and
17
- verified. Do not open a public issue with exploit details, credentials, workflow
18
- payloads, or identifying instance data.
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.literal(INTROSPECT_SCHEMA_VERSION),
223
- engineVersion: z.literal(INTROSPECT_ENGINE_VERSION),
224
- status: z.enum(["complete", "partial"]),
225
- profile: z.enum(["quick", "deep"]),
226
- workflow: z.object({
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
- sample: z.object({
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
- summary: z.object({
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
- metrics: z.object({
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
- findings: z.array(FindingSchema).max(1_000),
301
- ruleCoverage: z.array(RuleCoverageSchema).max(100),
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
- guidance: z.object({
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
- for (const [key, child] of entries.slice(0, 1_000)) {
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
- if (!preserveChildKeys && SENSITIVE_KEY.test(key) && !STRUCTURAL_IDENTIFIER_KEY.test(key)) {
201
- output[key] = "[REDACTED]";
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[key] = "[REDACTED]";
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[key] = "[REDACTED]";
258
+ output[outputKey] = "[REDACTED]";
212
259
  state.redacted = true;
213
260
  continue;
214
261
  }
215
262
  if (key === "value" && sensitiveSibling) {
216
- output[key] = "[REDACTED]";
263
+ output[outputKey] = "[REDACTED]";
217
264
  state.redacted = true;
218
265
  continue;
219
266
  }
220
- if (preserveChildKeys && SENSITIVE_KEY.test(key)) {
267
+ if (preserveChildKeys &&
268
+ (SENSITIVE_KEY.test(key) || (structuralKey === "properties" && sanitizedKey.redacted))) {
221
269
  if (structuralKey === "properties" && child !== null && typeof child === "object") {
222
- output[key] = sanitizeValue(child, state, depth + 1, key, false, true);
270
+ output[outputKey] = sanitizeValue(child, state, depth + 1, key, false, true);
223
271
  }
224
272
  else {
225
- output[key] = "[REDACTED]";
273
+ output[outputKey] = "[REDACTED]";
226
274
  state.redacted = true;
227
275
  }
228
276
  continue;
229
277
  }
230
- output[key] = sanitizeValue(child, state, depth + 1, key, STRUCTURAL_MAP_KEY.test(key), false);
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 sanitizeForOutput(value) {
237
- const state = { nodes: 0, redacted: false };
238
- return { data: sanitizeValue(value, state, 0), redacted: state.redacted, untrusted: true };
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 Error("The sanitized result exceeds the output limit. Use narrower filters or pagination.");
326
+ throw new OutputLimitError();
251
327
  }
252
328
  return text;
253
329
  }
@@ -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
- }