Skip to content

fix(fields): accept custom and third-party field types (v2.4.1)#7

Merged
zackkatz merged 12 commits into
mainfrom
develop
Jun 26, 2026
Merged

fix(fields): accept custom and third-party field types (v2.4.1)#7
zackkatz merged 12 commits into
mainfrom
develop

Conversation

@zackkatz

@zackkatz zackkatz commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

gf_add_field rejected any field type not in the static 46-key registry, throwing Unknown field type. That blocked third-party field types (Gravity Perks, Gravity Wiz, other add-ons) and GravityKit/custom fields that Gravity Forms itself accepts on save. The other paths already tolerated unknown types, so the behavior was inconsistent.

This makes the registry an enhancement source, not a gate, and aligns every field path on the same "tolerate unknown types" rule.

Release: 2.4.0 → 2.4.1 (patch).

Changes

  • gf_add_field graceful degradation (src/field-operations/field-manager.js): unknown types are created from the caller's properties (caller-supplied inputs/choices preserved), skipping registry-derived defaults/sub-inputs, and the result carries a warning naming the unrecognized type.
  • Stop leaking the dead _unknown flag (src/config/field-validation.js): it was written onto unknown-type fields and PUT verbatim to Gravity Forms via gf_create_form/gf_update_form, yet no code reads it.
  • Align validateFieldConfig (src/field-definitions/field-registry.js): tolerate unknown types instead of returning isValid:false (the branch was dead via its only caller and contradicted the new decision).
  • Cleanup: converted the orphan test/field-operations-integration.test.js to exercise the real createFieldOperations(...) layer (removed ~118 lines of drifting mock that re-threw on unknown types and asserted a stale gf_list_field_types shape).

Testing (offline gate, all green)

  • npm run test:node — 357/357
  • npm run test:unit — 289/289
  • npm run test:field-validation, npm run test:views — pass
  • npm run lint:docs, npm run lint:package (publint) — pass

All behavior changes were written test-first (RED → GREEN) in test/field-manager.test.js, test/field-validation.test.js, test/forms.test.js, and test/field-registry.test.js.

Notes

  • develop was 2 commits behind main (the v2.4.0 release merge commits); content was identical, so this merges cleanly. After this lands, consider a maindevelop sync to reconcile history.
  • Not tagged/published here — v2.4.1 tag + npm publish remain manual release steps.

https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo

Summary by CodeRabbit

  • Bug Fixes
    • Improved support for custom/third-party Gravity Forms field types: unknown types are accepted without leaking internal markers, and compound sub-input IDs are correctly rebased.
    • Prevent updates that would break conditional logic unless explicitly forced.
    • Added hardening across validation and tool input handling, including safer loopback detection, safer URL/redirect validation, and masking Cookie values; improved resilience for malformed/cyclic data.
  • Tests
    • Expanded unit and integration coverage for the above behaviors and edge cases.
  • Chores
    • Version bumped to 2.4.1 with updated identity/changelog documentation.

gf_add_field threw "Unknown field type" for any type not in the static 46-key registry, rejecting third-party fields (Gravity Perks, Gravity Wiz, etc.) that Gravity Forms itself accepts. The registry is now an enhancement, not a gate: unknown types are created with graceful degradation plus a warning. Also stop leaking the internal _unknown flag to Gravity Forms on form create/update, and align validateFieldConfig to tolerate unknown types. Convert the orphan field-operations-integration harness to exercise the real field-operations layer.

Bump 2.4.0 -> 2.4.1.

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@zackkatz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 3 minutes and 36 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5326e77d-480a-4d12-a3b1-4bc6007fcc1f

📥 Commits

Reviewing files that changed from the base of the PR and between d893c35 and 4536ad4.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • src/gravity-forms-client.js
  • src/wp-client.js
  • test/client-hardening.test.js
  • test/wp-client.test.js

Walkthrough

The PR updates field validation, field creation, dependency handling, utility hardening, integration tests, and release metadata for version 2.4.1.

Changes

Field handling and validation hardening

Layer / File(s) Summary
Validation contract
src/config/field-validation.js, src/config/validation.js, src/field-definitions/field-registry.js, test/field-registry.test.js, test/field-validation.test.js, test/validation-hardening.test.js, test/forms.test.js
Unknown field types are accepted without _unknown, null-input guards were added, object-valued field filters are rejected, and form/request tests cover the updated field payload behavior.
FieldManager handling
src/field-operations/field-manager.js, src/field-operations/field-dependencies.js, src/field-operations/index.js, test/field-manager.test.js, test/field-dependencies.test.js, test/field-operations-integration.test.js
addField now tolerates unknown types with warnings, updateField blocks dependency-breaking writes unless forced, and dependency scans and checks handle invalid inputs.
Utility hardening
src/utils/compact.js, src/utils/sanitize.js, src/server-runtime.js, src/config/auth.js, src/config/validation-config.js, src/abilities/loader.js, src/gravity-forms-client.js, test/compact.test.js, test/sanitize-hardening.test.js, test/server-runtime.test.js, test/validation-hardening.test.js, test/abilities-loader.test.js, test/client-hardening.test.js
Cycle-safe recursion, null-input handling, placeholder replacement, URL handling, auth/client configuration, and filtered tool lists were updated with matching tests.
Integration coverage
test/field-operations-integration.test.js, test/forms.test.js
The integration setup uses the production field-operations wiring, and the tests assert the updated field-type and request-payload behavior.
Release metadata
AGENTS.md, CHANGELOG.md, mcp.json, package.json, .github/workflows/publish.yml
Version references were updated to 2.4.1, the changelog adds the new release entry and link, and the publish workflow adds release announcement handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • GravityKit/MCP#5: This PR further tightens src/config/auth.js loopback handling, which is directly related to the URL and local-host validation hardening in this change.
  • GravityKit/MCP#6: This PR continues the same field-model and field-operations work by changing field-registry and field-manager behavior in the same areas.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: accepting custom and third-party field types in a v2.4.1 patch release.
Docstring Coverage ✅ Passed Docstring coverage is 95.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Comment @coderabbitai help to get the list of available commands.

@zackkatz

Copy link
Copy Markdown
Member Author

Live-verified against real Gravity Forms (local dev.test), beyond the offline suite:

  • Created a throwaway form, then gf_add_field with a non-registry type mailpot_custom — previously this threw Unknown field type; now it returns success with a warning, and a REST re-fetch confirms GF persisted it: ["text","address","mailpot_custom"].
  • Regression: known compound type address still generated its 6 sub-inputs.
  • Temporary form deleted afterward (clean).

Result: ALL PASS.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/field-operations/field-manager.js`:
- Around line 35-52: The addField flow in FieldManager still preserves
caller-supplied inputs for unknown types without updating their parent IDs after
generateFieldId assigns the new fieldId. Update addField to rebase any dotted
sub-inputs in the created field (especially for unknown types) so their IDs and
parent references are derived from the newly generated fieldId before saving,
while keeping generateSubInputs behavior for known compound types unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c853d00c-84e2-4b29-9ad0-d853f428860b

📥 Commits

Reviewing files that changed from the base of the PR and between 8beb4ba and fea0977.

📒 Files selected for processing (12)
  • AGENTS.md
  • CHANGELOG.md
  • mcp.json
  • package.json
  • src/config/field-validation.js
  • src/field-definitions/field-registry.js
  • src/field-operations/field-manager.js
  • test/field-manager.test.js
  • test/field-operations-integration.test.js
  • test/field-registry.test.js
  • test/field-validation.test.js
  • test/forms.test.js

Comment thread src/field-operations/field-manager.js
For unknown/third-party compound fields, gf_add_field preserved caller inputs verbatim, so dotted sub-input ids kept their assumed parent (e.g. 9.1) instead of the id generateFieldId assigns, orphaning the sub-inputs. Rebase dotted ids onto the generated field id (mirrors assignFieldIds); known compound types keep generateSubInputs unchanged. Live-verified on dev.test: 99.x inputs persist as 4.x.

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
@zackkatz

Copy link
Copy Markdown
Member Author

Addressed review feedback (sub-input id rebasing):

gf_add_field preserved caller-supplied inputs verbatim for unknown/third-party compound fields, so dotted sub-input ids kept their assumed parent (e.g. 9.1) instead of the id generateFieldId assigns — orphaning the sub-inputs. Now rebased onto the generated field id (mirrors assignFieldIds); known compound types are unchanged (generateSubInputs).

New unit test catches the mismatch (caller 9.x → field id 4 → 4.x), plus a regression guard for known compound types. Live-verified on dev.test: caller 99.1/99.2 persisted as 4.1/4.2. Offline gate green (test:node 358, test:unit 289).

…rsistence

Adversarial pass (50+ probes) surfaced 15 defects, all fixed test-first: gf_update_field saved a change before reporting it blocked (dependency guard now runs before the write, like deleteField); gf_add_field crashed on null properties/position and accepted empty/null/non-string field_type; gf_create_form fields:[null] threw an opaque TypeError instead of a clean error; null-input crashes in validateFieldConfig/detectFieldVariant/generateCompoundInputs/stripEntryMeta/DependencyTracker; stripEmpty/sanitize could stack-overflow on circular input; buildToolList emitted an undefined entry when gkReloadDef was omitted; validateFieldFilter silently stringified object values. Offline gate green (test:node 377, test:unit 100%, field-validation, publint, lint:docs); adversarial flags 13->3 (remaining 3 are non-bugs); live-verified on dev.test.

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
@zackkatz

Copy link
Copy Markdown
Member Author

Adversarial hardening pass (15 bugs fixed, test-first)

Ran an adversarial probe harness (89 probes over two rounds) against the real modules; fixed 15 genuine defects RED→GREEN:

Reachable (agent-controlled input):

  1. gf_add_field crashed on properties: null (TypeError).
  2. gf_add_field crashed on position: null (TypeError).
  3. gf_add_field silently accepted empty/null/non-string field_type (created a typeless field).
  4. gf_create_form with fields: [null] threw an opaque TypeError instead of a clean "must be an object" error.
  5. gf_update_field persisted a change it then reported as blocked — the dependency guard ran after the write. Now gated before the write (like gf_delete_field).
  6. validateFieldFilter silently turned an object value into "[object Object]" instead of rejecting it.

Defensive (exported helpers crashed on null/cyclic input):
6–11. validateFieldConfig / detectFieldVariant / generateCompoundInputs / stripEntryMeta / DependencyTracker.scanFormDependencies / hasBreakingDependencies.
12–13. sanitize() / stripEmpty() stack-overflow on circular input (cycle guard).
14. buildToolList emitted an undefined entry when gkReloadDef was omitted.

Verification: offline gate green (test:node 377, test:unit 100%, field-validation, publint, lint:docs); harness flags 13→3 (the 3 remaining are non-bugs: pathological 100k-deep nesting + two params gf_list_entries correctly ignores); field-op fixes live-verified on dev.test. No version change (still 2.4.1).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utils/sanitize.js (1)

46-65: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Shared references are now treated as cycles and lose data.

Because seen is a WeakSet, any second visit is cut off, even for acyclic graphs that legitimately reuse the same object. { a: child, b: child } now produces a sanitized a but collapses b to {}/[]. If you only want to break recursion, cache source→sanitized clone in a WeakMap and return the cached clone on revisit.

Suggested fix
-export function sanitize(obj, seen = new WeakSet()) {
+export function sanitize(obj, seen = new WeakMap()) {
   if (!obj || typeof obj !== 'object') return obj;

-  if (seen.has(obj)) return Array.isArray(obj) ? [] : {};
-  seen.add(obj);
+  if (seen.has(obj)) return seen.get(obj);

   if (Array.isArray(obj)) {
-    return obj.map((v) => sanitize(v, seen));
+    const out = [];
+    seen.set(obj, out);
+    out.push(...obj.map((v) => sanitize(v, seen)));
+    return out;
   }

   const result = {};
+  seen.set(obj, result);
   for (const [key, value] of Object.entries(obj)) {
     const keyLower = key.toLowerCase();
     const isSensitive = SENSITIVE_KEYS.some(k => keyLower.includes(k));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/sanitize.js` around lines 46 - 65, The sanitize helper is treating
any repeated object reference as a cycle, so shared acyclic objects get
collapsed and lose data. Update sanitize to preserve shared references by
replacing the current WeakSet-based revisit check with a source-to-clone cache
(for example in sanitize and its recursive calls) so a previously seen object
returns the same sanitized clone instead of an empty placeholder. Keep the
existing recursion guard behavior for true cycles while ensuring repeated
references under objects and arrays still serialize consistently.
🧹 Nitpick comments (1)
test/compact.test.js (1)

19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the recursive edge too, not just the root fields.

This test still passes if stripEmpty() returns the original object at out.self, which is the current regression. Add an assertion on the recursive branch as well, e.g. out.self.b === undefined or out.self !== o, so the cycle fix is actually enforced.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/compact.test.js` around lines 19 - 25, The circular-reference test for
stripEmpty only checks the root object fields, so it can still pass if the
recursive branch incorrectly points back to the original object. Update the test
in stripEmpty to also assert the cyclic path returned from stripEmpty(o) is
fixed, using out.self and the stripEmpty symbol, by verifying the recursive
branch no longer preserves the original object state (for example, that
out.self.b is undefined or that out.self is not the original o).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/field-operations/field-manager.js`:
- Around line 121-132: updateField in field-manager.js is only checking
dependencies.conditionalLogic, which misses other breaking dependencies handled
by deleteField. Update the dependency gate to use
this.dependencyTracker.hasBreakingDependencies(dependencies) in the updateField
flow so the same blocking rules apply consistently, and keep the existing force
behavior and error response structure intact.

In `@src/utils/compact.js`:
- Around line 15-27: The revisit handling in stripEmpty is returning the
original input node for already-seen arrays/objects, which can leak unstripped
values through circular references. Update stripEmpty to use a source-to-clone
cache so repeated visits return the already-built sanitized clone instead of the
source object/array. Keep the fix within stripEmpty and preserve the current
null/empty-string filtering while ensuring self-references and repeated
references resolve to the cleaned result.

---

Outside diff comments:
In `@src/utils/sanitize.js`:
- Around line 46-65: The sanitize helper is treating any repeated object
reference as a cycle, so shared acyclic objects get collapsed and lose data.
Update sanitize to preserve shared references by replacing the current
WeakSet-based revisit check with a source-to-clone cache (for example in
sanitize and its recursive calls) so a previously seen object returns the same
sanitized clone instead of an empty placeholder. Keep the existing recursion
guard behavior for true cycles while ensuring repeated references under objects
and arrays still serialize consistently.

---

Nitpick comments:
In `@test/compact.test.js`:
- Around line 19-25: The circular-reference test for stripEmpty only checks the
root object fields, so it can still pass if the recursive branch incorrectly
points back to the original object. Update the test in stripEmpty to also assert
the cyclic path returned from stripEmpty(o) is fixed, using out.self and the
stripEmpty symbol, by verifying the recursive branch no longer preserves the
original object state (for example, that out.self.b is undefined or that
out.self is not the original o).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e9245f6d-5fa5-4d09-bfa4-3e16c4904922

📥 Commits

Reviewing files that changed from the base of the PR and between fea0977 and 7838d78.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • src/config/field-validation.js
  • src/config/validation.js
  • src/field-definitions/field-registry.js
  • src/field-operations/field-dependencies.js
  • src/field-operations/field-manager.js
  • src/field-operations/index.js
  • src/server-runtime.js
  • src/utils/compact.js
  • src/utils/sanitize.js
  • test/compact.test.js
  • test/field-dependencies.test.js
  • test/field-manager.test.js
  • test/field-registry.test.js
  • test/sanitize-hardening.test.js
  • test/server-runtime.test.js
  • test/validation-hardening.test.js
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/config/field-validation.js
  • test/field-registry.test.js

Comment on lines 121 to +132
const dependencies = this.dependencyTracker?.scanFormDependencies(form, fieldId) || {};

const hasBreakingDeps = dependencies.conditionalLogic?.length > 0;

if (hasBreakingDeps && !force) {
return {
success: false,
error: 'Field has dependencies that may be affected',
field_id: fieldId,
dependencies,
suggestion: 'Use force=true to update anyway'
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect relevant file and nearby dependency tracker implementation
git ls-files 'src/field-operations/**' | sed -n '1,120p'
echo '--- field-manager outline ---'
ast-grep outline src/field-operations/field-manager.js --view expanded || true
echo '--- dependency tracker outline ---'
fd -a 'dependency-tracker.js|dependencyTracker.js|*dependency*' src/field-operations src || true

Repository: GravityKit/MCP

Length of output: 1405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant sections with line numbers
wc -l src/field-operations/field-manager.js
cat -n src/field-operations/field-manager.js | sed -n '1,240p'

echo '--- dependency tracker candidates ---'
fd -a 'dependency-tracker.js|dependencyTracker.js|*dependency*' src

Repository: GravityKit/MCP

Length of output: 10784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect dependency tracker behavior precisely
fd -a 'dependency-tracker.js|dependencyTracker.js|*dependency*' src || true
rg -n "hasBreakingDependencies|scanFormDependencies|conditionalLogic|calculations|mergeTags|merge tag|merge-tag" src/field-operations src || true

Repository: GravityKit/MCP

Length of output: 8964


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant implementation and dependency handling
wc -l src/field-operations/field-manager.js
cat -n src/field-operations/field-manager.js | sed -n '1,240p'

echo '--- dependency tracker files ---'
fd -a 'dependency-tracker.js|dependencyTracker.js|*dependency*' src || true

echo '--- dependency-related symbols ---'
rg -n "hasBreakingDependencies|scanFormDependencies|conditionalLogic|calculations|mergeTags|merge tag|merge-tag" src/field-operations src || true

Repository: GravityKit/MCP

Length of output: 19248


updateField should use the same dependency gate as deleteField.
deleteField treats conditional logic, calculations, and merge tags as breaking dependencies, but updateField only blocks on dependencies.conditionalLogic?.length > 0. That still allows updates that can break calculations or merge-tag references. Reuse this.dependencyTracker.hasBreakingDependencies(dependencies) for consistency, or narrow the comment if conditional logic is the only intended blocker.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/field-operations/field-manager.js` around lines 121 - 132, updateField in
field-manager.js is only checking dependencies.conditionalLogic, which misses
other breaking dependencies handled by deleteField. Update the dependency gate
to use this.dependencyTracker.hasBreakingDependencies(dependencies) in the
updateField flow so the same blocking rules apply consistently, and keep the
existing force behavior and error response structure intact.

Comment thread src/utils/compact.js
Comment on lines +15 to +27
export function stripEmpty(obj, seen = new WeakSet()) {
if (Array.isArray(obj)) {
return obj.map(stripEmpty);
if (seen.has(obj)) return obj;
seen.add(obj);
return obj.map((v) => stripEmpty(v, seen));
}
if (obj !== null && typeof obj === 'object') {
if (seen.has(obj)) return obj;
seen.add(obj);
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === '') continue;
result[key] = stripEmpty(value);
result[key] = stripEmpty(value, seen);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Returning the original node on revisit leaks unstripped data.

On Line 22 (and similarly Line 17), a repeated visit returns the original input object/array, so the output can still contain values this helper was supposed to remove. With a self-reference like { b: '', self: obj }, out.self.b remains '' because self points back to the source object. Use a source→clone cache so revisits return the already-built sanitized clone instead of the input node.

Suggested fix
-export function stripEmpty(obj, seen = new WeakSet()) {
+export function stripEmpty(obj, seen = new WeakMap()) {
   if (Array.isArray(obj)) {
-    if (seen.has(obj)) return obj;
-    seen.add(obj);
-    return obj.map((v) => stripEmpty(v, seen));
+    if (seen.has(obj)) return seen.get(obj);
+    const out = [];
+    seen.set(obj, out);
+    out.push(...obj.map((v) => stripEmpty(v, seen)));
+    return out;
   }
   if (obj !== null && typeof obj === 'object') {
-    if (seen.has(obj)) return obj;
-    seen.add(obj);
+    if (seen.has(obj)) return seen.get(obj);
     const result = {};
+    seen.set(obj, result);
     for (const [key, value] of Object.entries(obj)) {
       if (value === null || value === '') continue;
       result[key] = stripEmpty(value, seen);
     }
     return result;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function stripEmpty(obj, seen = new WeakSet()) {
if (Array.isArray(obj)) {
return obj.map(stripEmpty);
if (seen.has(obj)) return obj;
seen.add(obj);
return obj.map((v) => stripEmpty(v, seen));
}
if (obj !== null && typeof obj === 'object') {
if (seen.has(obj)) return obj;
seen.add(obj);
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === '') continue;
result[key] = stripEmpty(value);
result[key] = stripEmpty(value, seen);
export function stripEmpty(obj, seen = new WeakMap()) {
if (Array.isArray(obj)) {
if (seen.has(obj)) return seen.get(obj);
const out = [];
seen.set(obj, out);
out.push(...obj.map((v) => stripEmpty(v, seen)));
return out;
}
if (obj !== null && typeof obj === 'object') {
if (seen.has(obj)) return seen.get(obj);
const result = {};
seen.set(obj, result);
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === '') continue;
result[key] = stripEmpty(value, seen);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/compact.js` around lines 15 - 27, The revisit handling in
stripEmpty is returning the original input node for already-seen arrays/objects,
which can leak unstripped values through circular references. Update stripEmpty
to use a source-to-clone cache so repeated visits return the already-built
sanitized clone instead of the source object/array. Keep the fix within
stripEmpty and preserve the current null/empty-string filtering while ensuring
self-references and repeated references resolve to the cleaned result.

zackkatz added 3 commits June 25, 2026 21:53
… validation

Five more genuine defects, fixed test-first: isLocalUrl treated remote 127.x domains (e.g. 127.example.com) as local, which could allow Basic auth over plain HTTP; sanitize/sanitizeHeaders never masked Cookie; validateURL rejected dotless hosts (localhost/intranet) for confirmation redirects; assignFieldIds let duplicate explicit field ids survive; formatErrorMessage only replaced the first {field} and interpreted $-sequences in values. Offline gate green (test:node 384, test:unit 100%, auth, field-validation, publint, lint:docs).

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
…N_WEBHOOK

Adds an "Announce release to Slack" step after "Create GitHub Release"
that POSTs the gktools {link,version,changelog,product} contract to
SLACK_RELEASE_NOTIFICATION_WEBHOOK, so GravityKit MCP shows up in
#release-announcements like every other GravityKit plugin (which get
this for free via gktools' "Notify Slack" block).

- product = "GravityKit MCP"; version reuses steps.guard.outputs.version;
  link = .../releases/tag/v<version>; changelog = the CHANGELOG.md section
  for that version (awk between "## [<version>]" and the next "## [").
- jq builds the JSON so changelog newlines/quotes are safely escaped.
- Non-fatal, mirroring gktools: a missing webhook secret skips cleanly,
  and a response without "ok":true only warns. The release never fails on
  Slack.
- Gated on a new steps.release.outputs.created output so it fires only when
  a release was actually created this run (not on an idempotent skip).

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
In test mode resolveEnv() remaps GRAVITY_FORMS_TEST_TIMEOUT into this.config, but the GravityFormsClient constructor read the raw config object, so the test-site timeout override was silently ignored (stayed 30000). Read timeout and self-signed-cert settings from this.config like every other setting. Found by reading the constructor; verified test-first. Gate green (test:node 385).

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/field-definitions/field-registry.js (1)

1001-1024: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Rebase dotted sub-input IDs even when the explicit field ID is kept.

Line 1002 returns the field unchanged for the first valid explicit id, so a payload like { id: 7, inputs: [{ id: '2.3' }] } keeps compound inputs anchored to 2 instead of the final parent id 7. That can still orphan third-party compound inputs even though the field id itself is valid.

Proposed fix
+const rebaseInputs = (fieldId, inputs) => inputs.map((input) => {
+  const hasDottedId = input && typeof input.id === 'string' && input.id.includes('.');
+  if (!hasDottedId) {
+    return input;
+  }
+  const sub = input.id.slice(input.id.indexOf('.') + 1);
+  return { ...input, id: `${fieldId}.${sub}` };
+});
+
 export function assignFieldIds(fields) {
   if (!Array.isArray(fields)) {
     return fields;
   }
@@
   return fields.map((field) => {
     const id = Number(field?.id);
     const hasFreshExplicitId = Number.isInteger(id) && id > 0 && !claimed.has(id);
     if (hasFreshExplicitId) {
       claimed.add(id);
-      return field;
+      if (Array.isArray(field?.inputs)) {
+        return { ...field, inputs: rebaseInputs(id, field.inputs) };
+      }
+      return field;
     }
@@
     // Re-base any provided compound sub-input ids onto the new field id.
     if (Array.isArray(field?.inputs)) {
-      const inputs = field.inputs.map((input) => {
-        const hasDottedId = input && typeof input.id === 'string' && input.id.includes('.');
-        if (hasDottedId) {
-          const sub = input.id.slice(input.id.indexOf('.') + 1);
-          return { ...input, id: `${newId}.${sub}` };
-        }
-        return input;
-      });
+      const inputs = rebaseInputs(newId, field.inputs);
       return { ...field, id: newId, inputs };
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/field-definitions/field-registry.js` around lines 1001 - 1024, The
explicit-id fast path in field-registry’s field reassignment logic still leaves
dotted compound input IDs attached to the old parent. Update the branch around
the fresh explicit id check and the input rebasing logic so that whenever a
field with inputs is accepted, its dotted sub-input ids are rewritten to use the
final parent id, not just when a new id is allocated. Use the existing field/id
handling in the same function to keep the rebase behavior consistent for both
explicit and generated ids.
🧹 Nitpick comments (1)
.github/workflows/publish.yml (1)

117-118: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Bind ${{ }} expressions to env vars to avoid template injection into the shell.

steps.guard.outputs.version is interpolated directly into the script body before bash parses it. Although the value originates from the repo's own package.json, GitHub expands ${{ }} textually before execution, so a crafted version string would land inside the shell script. The hardened pattern is to pass these through the env: block and reference them as quoted shell variables.

🔒 Proposed env-binding
         env:
           SLACK_RELEASE_NOTIFICATION_WEBHOOK: ${{ secrets.SLACK_RELEASE_NOTIFICATION_WEBHOOK }}
+          VERSION: ${{ steps.guard.outputs.version }}
+          REPOSITORY: ${{ github.repository }}
         run: |
           if [ -z "${SLACK_RELEASE_NOTIFICATION_WEBHOOK:-}" ]; then
             echo "SLACK_RELEASE_NOTIFICATION_WEBHOOK not set — skipping Slack announcement."
             exit 0
           fi
-          VERSION="${{ steps.guard.outputs.version }}"
-          LINK="https://github.com/${{ github.repository }}/releases/tag/v$VERSION"
+          LINK="https://github.com/$REPOSITORY/releases/tag/v$VERSION"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/publish.yml around lines 117 - 118, The publish workflow
script is interpolating the guard version directly into the shell body, which
can expose the job to template-to-shell injection. Update the publish step to
bind steps.guard.outputs.version through the env block and reference it as a
quoted shell variable when building VERSION and LINK, instead of using GitHub
expression syntax inline. Keep the fix localized to the publish job/script
section and ensure any later use of VERSION/LINK in that step reads from
env-backed shell vars.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/field-definitions/field-registry.js`:
- Around line 1001-1024: The explicit-id fast path in field-registry’s field
reassignment logic still leaves dotted compound input IDs attached to the old
parent. Update the branch around the fresh explicit id check and the input
rebasing logic so that whenever a field with inputs is accepted, its dotted
sub-input ids are rewritten to use the final parent id, not just when a new id
is allocated. Use the existing field/id handling in the same function to keep
the rebase behavior consistent for both explicit and generated ids.

---

Nitpick comments:
In @.github/workflows/publish.yml:
- Around line 117-118: The publish workflow script is interpolating the guard
version directly into the shell body, which can expose the job to
template-to-shell injection. Update the publish step to bind
steps.guard.outputs.version through the env block and reference it as a quoted
shell variable when building VERSION and LINK, instead of using GitHub
expression syntax inline. Keep the fix localized to the publish job/script
section and ensure any later use of VERSION/LINK in that step reads from
env-backed shell vars.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0db1b32a-a55c-41af-8e9d-89126aa55c27

📥 Commits

Reviewing files that changed from the base of the PR and between 7838d78 and 265df94.

📒 Files selected for processing (9)
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • src/config/auth.js
  • src/config/validation-config.js
  • src/field-definitions/field-registry.js
  • src/utils/sanitize.js
  • test/field-registry.test.js
  • test/sanitize-hardening.test.js
  • test/validation-hardening.test.js
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/field-registry.test.js
  • src/utils/sanitize.js

zackkatz added 4 commits June 25, 2026 22:15
…en publish.yml

Three more genuine bugs, test-first: (1) assignFieldIds infinite-loops on an out-of-range field id (1e308 etc.) because next++ can't advance past float precision — now only safe integers count as explicit ids (DoS, found via fuzzing). (2) assignFieldIds preserved-explicit-id fast path left dotted sub-inputs attached to a stale parent — now rebases inputs to the final id in both branches via a shared helper. (3) publish.yml Slack step interpolated steps.guard.outputs.version / github.repository directly into the shell body — now bound via env and referenced as quoted shell vars (template-to-shell injection). Gate green (test:node 387, test:unit 100%, publish.yml YAML/actionlint clean).

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
methodForAbility(null) threw 'Cannot read properties of null (reading readonly)' — the = {} default only covers undefined. Now guarded with optional chaining (returns POST). Surfaced by a fuzz pass over the exported surface. Verified test-first.

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
…ge id fixes [ci skip]

Reflect the net user-facing 2.4.1 changes since v2.4.0: compound sub-input ids stay consistent with the field id, and out-of-range/duplicate explicit field ids are corrected (an out-of-range id no longer stalls form creation). CI/release tooling (Slack announce), the test-only timeout fix, and the internal methodForAbility guard are intentionally omitted (not user-facing).

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gravity-forms-client.js`:
- Line 116: The TLS self-signed certificate check in the Gravity Forms client is
still using string truthiness, so one flag can mask the other. Update the
`rejectUnauthorized` logic in `src/gravity-forms-client.js` to evaluate
`this.config.GRAVITY_FORMS_ALLOW_SELF_SIGNED_CERTS` and
`this.config.MCP_ALLOW_SELF_SIGNED_CERTS` explicitly as booleans/strings, and
allow self-signed certs when either setting is enabled. Use the
`GravityFormsClient` config handling around `rejectUnauthorized` to locate and
fix the conditional.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f8dbaca3-39d4-4463-abd0-6f81abe52bf1

📥 Commits

Reviewing files that changed from the base of the PR and between 265df94 and d893c35.

📒 Files selected for processing (8)
  • .github/workflows/publish.yml
  • CHANGELOG.md
  • src/abilities/loader.js
  • src/field-definitions/field-registry.js
  • src/gravity-forms-client.js
  • test/abilities-loader.test.js
  • test/client-hardening.test.js
  • test/field-registry.test.js
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • .github/workflows/publish.yml
  • test/field-registry.test.js
  • src/field-definitions/field-registry.js

Comment thread src/gravity-forms-client.js Outdated
zackkatz added 2 commits June 25, 2026 22:36
rejectUnauthorized used (A || B) !== 'true', but 'false' is a truthy string, so GRAVITY_FORMS_ALLOW_SELF_SIGNED_CERTS='false' short-circuited and masked MCP_ALLOW_SELF_SIGNED_CERTS='true' (and vice versa). Now each flag is compared === 'true' separately, in both GravityFormsClient and WordPressClient (same bug). Test-first; gate green (test:node 389).

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
Drop the internal-marker bullet (not customer-facing) ahead of the 2.4.1 release.

Claude-Session: https://claude.ai/code/session_01LJHfTpQknHFs1j5ayj7fpo
@zackkatz zackkatz merged commit 6aa7b79 into main Jun 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant