Fix confirmations and notifications validation to accept objects - #3
Conversation
The validateRestApiAccess function was calling getAuthHeaders() without the required URL parameter for OAuth 1.0a signature generation. This caused authentication failures when using HTTP connections (which fall back to OAuth instead of Basic Auth). Changes: - Get baseURL from httpClient.defaults.baseURL - Construct full URL for each endpoint - Pass proper parameters (method, url, params) to getAuthHeaders() This fixes the "REST API validation failed: undefined" error when connecting via HTTP/OAuth.
The Gravity Forms REST API v2 returns confirmations and notifications as objects keyed by ID, not arrays. The validation was incorrectly using validateArray()/`.array()` which rejected the correct format. Changed both validators to use validateObject()/`.object()` and added missing notifications validation.
WalkthroughRefactors confirmations/notifications from arrays to keyed objects across validation, schema, tests, and test data; updates OAuth signing to build and validate full endpoint URLs from Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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/config/auth.js (1)
327-345:⚠️ Potential issue | 🟡 Minor
baseURLrequirement applies universally even for Basic Auth.
BasicAuthHandler.getAuthHeaders()accepts no arguments and ignoresfullUrl, so the mandatorybaseURLcheck at lines 323–325 adds a new runtime requirement forvalidateRestApiAccesseven in Basic Auth deployments. A customhttpClientthat lacksdefaults.baseURLwould cause this health-check to fail. Consider short-circuiting the check when the active auth handler isBasicAuthHandler.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/auth.js` around lines 327 - 345, The health-check adds a mandatory baseURL requirement even for BasicAuth deployments; update validateRestApiAccess to detect when the active auth handler is BasicAuthHandler (e.g., via authManager.activeHandler or checking authManager.getAuthHeaders implementation/constructor name) and skip the baseURL existence check in that case, and when calling authManager.getAuthHeaders use the signature appropriate for the handler (call getAuthHeaders('GET', fullUrl, ...) only for handlers that accept a full URL, otherwise call BasicAuthHandler.getAuthHeaders() with no args), keeping the httpClient.get call using endpoint.path and not assuming httpClient.defaults.baseURL exists.
🧹 Nitpick comments (1)
src/config/validators.js (1)
90-106:!Array.isArray(confirmations)guard is redundant after.object().The
.object()chain rule already rejects arrays before the custom callback runs, making this condition unreachable. Theconfirmations &&null/undefined guard is still needed, but the array check can be dropped.♻️ Proposed simplification
.custom((confirmations) => { - if (confirmations && typeof confirmations === 'object' && !Array.isArray(confirmations)) { + if (confirmations && typeof confirmations === 'object') { Object.entries(confirmations).forEach(([key, conf]) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/validators.js` around lines 90 - 106, In the custom validator attached to field('confirmations') (the validate('confirmations').object().custom(...) block), remove the redundant Array.isArray(confirmations) check since .object() already rejects arrays; keep the null/undefined guard (confirmations && typeof confirmations === 'object') so the custom logic still skips when confirmations is missing, and otherwise iterate Object.entries(confirmations) to validate redirect entries as before (i.e., call validate(`confirmations.${key}.url`).required().string().url().validate(conf.url) when conf.type === 'redirect' and conf.url !== undefined).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/config/auth.js`:
- Around line 320-325: The code reads httpClient.defaults.baseURL directly which
can throw if httpClient.defaults is undefined and get swallowed by the outer
catch; change the lookup to use optional chaining (e.g., access
httpClient?.defaults?.baseURL) and keep the existing explicit guard that throws
'httpClient baseURL is not configured' when baseURL is falsy so the intended
error message is produced; update the reference in the same block where baseURL
is declared (the httpClient.defaults/baseURL access) to use optional chaining.
In `@src/config/validation.js`:
- Around line 311-317: validateObject currently rejects arrays, but
formData.confirmations is treated as an array in multiple tests and scripts;
update all places that set confirmations to use the keyed-object Gravity Forms
v2 shape instead of arrays. Replace array usage in
src/tests/integration.test.js, src/tests/validation.test.js, and
scripts/setup-test-data.js so confirmations are objects keyed like
"confirmation_1": { type: ..., url: ... } (so
validateObject(formData.confirmations, 'confirmations') and the subsequent loop
that calls validateURL(conf.url, `confirmations.${key}.url`) will work without
errors).
---
Outside diff comments:
In `@src/config/auth.js`:
- Around line 327-345: The health-check adds a mandatory baseURL requirement
even for BasicAuth deployments; update validateRestApiAccess to detect when the
active auth handler is BasicAuthHandler (e.g., via authManager.activeHandler or
checking authManager.getAuthHeaders implementation/constructor name) and skip
the baseURL existence check in that case, and when calling
authManager.getAuthHeaders use the signature appropriate for the handler (call
getAuthHeaders('GET', fullUrl, ...) only for handlers that accept a full URL,
otherwise call BasicAuthHandler.getAuthHeaders() with no args), keeping the
httpClient.get call using endpoint.path and not assuming
httpClient.defaults.baseURL exists.
---
Nitpick comments:
In `@src/config/validators.js`:
- Around line 90-106: In the custom validator attached to field('confirmations')
(the validate('confirmations').object().custom(...) block), remove the redundant
Array.isArray(confirmations) check since .object() already rejects arrays; keep
the null/undefined guard (confirmations && typeof confirmations === 'object') so
the custom logic still skips when confirmations is missing, and otherwise
iterate Object.entries(confirmations) to validate redirect entries as before
(i.e., call
validate(`confirmations.${key}.url`).required().string().url().validate(conf.url)
when conf.type === 'redirect' and conf.url !== undefined).
- Use optional chaining for httpClient.defaults.baseURL access to prevent TypeError when defaults is undefined (auth.js) - Remove redundant Array.isArray guard in validators.js since .object() already rejects arrays - Convert confirmations from array to keyed-object format in tests and setup scripts to match Gravity Forms API v2 structure
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/tests/validation.test.js (1)
240-258: Consider adding a type validation test fornotifications.The existing "Type Validation: Objects" tests cover
searchandmeta, but there's no explicit test asserting thatnotificationsrejects non-object values (e.g., passing a string or array). Sincenotificationsis a newly added validated field, a quick test would solidify coverage.💡 Suggested test addition (outside of current line range)
// Notifications should be object await TestAssert.throwsAsync( () => client.createForm({ title: 'Test', notifications: 'not-an-object' }), 'must be an object', 'Notifications parameter should be object' );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tests/validation.test.js` around lines 240 - 258, Add a type validation test asserting that the new notifications field rejects non-object values: in the existing "Type Validation: Objects" suite add a TestAssert.throwsAsync call that invokes client.createForm with notifications set to a non-object (e.g., a string or array) and expects the error message 'must be an object' and a descriptive assertion message like 'Notifications parameter should be object'; reference the client.createForm call and the notifications field when locating where to insert the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/tests/validation.test.js`:
- Around line 240-258: Add a type validation test asserting that the new
notifications field rejects non-object values: in the existing "Type Validation:
Objects" suite add a TestAssert.throwsAsync call that invokes client.createForm
with notifications set to a non-object (e.g., a string or array) and expects the
error message 'must be an object' and a descriptive assertion message like
'Notifications parameter should be object'; reference the client.createForm call
and the notifications field when locating where to insert the test.
|
@claude Verify each finding against the current code and only fix it if needed. Nitpick comments:
|
Add 1.0.4 and 1.0.5 changelog entries. 1.0.5 includes OAuth signature generation fix and confirmations/notifications validation fix (PR #3).
…— TDD All four findings fixed test-first (RED watched, then GREEN). Logic for the three index.js issues was extracted into src/server-runtime.js so it's unit-testable (index.js self-runs main() and isn't importable). - #1 serial init: WordPress plane now starts before the Gravity Forms REST probe is awaited, so a slow/bad GF config no longer stalls the WP plane or the abilities load. (runPlaneInit) - #2 tool advertising: gf_* + field-op tools are listed only when the GF plane is live, so a WP-only install doesn't advertise tools that error on call. (buildToolList, gated on gravityFormsClient) - #3 dispatch: the call router no longer hard-codes name.startsWith('gv_'). It routes by ability-handler-map membership, so any GravityKit product prefix (gc_, …) dispatches, not just GravityView's gv_. (classifyAbilityCall) - #4 wp-client: WordPressClient refuses to send Basic auth over a remote plain-HTTP URL (credential exposure) unless GRAVITY_FORMS_ALLOW_HTTP_BASIC_AUTH =true, reusing isLocalUrl — matching the GF plane's guard. Tests: test/server-runtime.test.js (10), test/wp-client.test.js (4), wired into test:node. server-runtime.js added to the AGENTS.md repo map. Verified: test:node 165, test:unit 269, prepublishOnly gate green; live MCP smoke (SDK client → node src/index.js) shows 76 tools (26 gf_ + 49 gv_ + gk_reload_abilities) and gv_layouts_list dispatches.
…ped in transit TICKET items GravityKit#2 and GravityKit#3. gf_submit_form_data reported every field empty ('This field is required' for all of them) and gf_update_entry returned a clean 200 over a no-op. Root cause proven before writing code (scripts/dynamic-key-probe.mjs): the values DO reach the server over stdio and land correctly. An MCP client strips loose top-level input_N / numeric keys before the request is sent, and additionalProperties:true — which BOTH tools already declared — does not protect them. Third instance of this failure mode today, after markupVersion and set_content's object params. Adds a declared `values` object to both tools, expanded server-side: bare '1', prefixed 'input_1' and sub-input '1.3' all normalise to GF's wire form. Loose keys still work and are merged first, so direct/stdio callers are unaffected. gf_update_entry now REFUSES an update carrying neither field values nor status, naming the stripping as the likely cause. That call could previously only be a no-op returning 200 — the exact shape that cost the ticket author a debugging session. Verified 8/8 on aaru65 (scripts/values-param-verify.mjs): submit lands and the value reads back off the created entry, input_-prefixed keys normalise, update lands, the empty update is refused, and a status-only update still works. One fixture note worth keeping: the first run failed the submit cases because aaru65 rejects @example.com addresses outright. That looked exactly like the values param not working. Fixture now uses a real domain. Suite unchanged at 11/16. Scratch forms 27 and 28 trashed; entry 2357 — the blank row the ticket left on the live waitlist, which this test had filled with a visible marker — moved to trash so it stops rendering in View 4839.
Summary
confirmationsandnotificationsvalidation to accept objects (keyed by ID) instead of arraysnotificationsvalidation in both legacy and new validator pathsProblem
The Gravity Forms REST API v2 returns
confirmationsandnotificationsas objects keyed by ID, not arrays. However, both validators incorrectly enforced array validation:validators.js: Used.array()on the confirmations fieldvalidation.js: CalledvalidateArray()on confirmationsThis caused all
gf_create_formandgf_update_formcalls with confirmations or notifications to fail with:Additionally,
notificationshad no validation at all in either validator, meaning they were silently stripped from the validated output.Steps to Reproduce
{ "title": "Test Form", "confirmations": { "abc123": { "id": "abc123", "name": "Default Confirmation", "type": "message", "message": "Thanks!" } } }Validation error: confirmations must be an arraySolution
validateArray()→validateObject()for confirmations invalidation.js.array()→.object()for confirmations invalidators.js.map(),.forEach()) toObject.entries()notificationsvalidation as.object()in both validatorsTest plan
gf_update_formwith confirmations object succeeds (previously failed)gf_update_formwith notifications object succeeds (previously stripped)Summary by CodeRabbit
New Features
Bug Fixes
Tests