Skip to content

Feat/escrows release funds#428

Merged
sotoJ24 merged 5 commits into
safetrustcr:mainfrom
KevinMB0220:feat/escrows-release-funds
Jul 2, 2026
Merged

Feat/escrows release funds#428
sotoJ24 merged 5 commits into
safetrustcr:mainfrom
KevinMB0220:feat/escrows-release-funds

Conversation

@KevinMB0220

@KevinMB0220 KevinMB0220 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

feat: implement escrow release-funds webhook handler and fix index routes

Description

This PR implements the webhook handler for POST /api/escrows/release-funds to handle TrustlessWork release callbacks and updates the escrow record status to completed and balance to 0. It also resolves several critical structure issues and import failures present on the main branch.

Key Changes

1. Escrow Release Funds Webhook

  • Handler (release-funds.handler.js): Processes callbacks with contractId and releaseSigner. Updates the escrow record in public.trustless_work_escrows table via a Hasura GraphQL mutation.
    • Resilient Design: Employs a fallback mechanism. It attempts the camelCase mutation first (update_trustlessWorkEscrows), and falls back to snake_case (update_trustless_work_escrows) if metadata configuration is not fully loaded.
  • Route (release-funds.route.js): Mounts POST /api/escrows/release-funds and exposes it.
  • Registration: Mounted in the root router before the Firebase auth middleware is applied to bypass token checks (since this is a server-to-server webhook callback).

2. Main Branch Fixes & Restructuring

  • Auth Routes Restored: Re-created the missing authentication synchronization handler (sync-user.handler.js), route, and index entrypoint which were absent or deleted on main.
  • Middleware Path Resolution: Fixed imports of the auth middleware to correctly locate auth.middleware.js instead of the non-existent auth.js.
  • Reconciliation entrypoint: Created routes/reconciliation/index.js to bundle reconciliation routes.
  • Table Tracking Fix: Updated deploy-tenant.sh to extract database table names directly from the YAML files instead of the file names, preventing Inconsistent object exceptions in Hasura during migrations.
  • YQ Tool Integration: Added dynamic, architecture-aware yq CLI downloader to docker-compose-test.yml during test runs so metadata is compiled and applied correctly inside the test container.

3. Verification & Tests

  • Seed Data (seed-test-escrows.sql): Seed script to populate a test escrow in the database with milestone_approved status and a balance of 3200.0.
  • Karate Feature (release-funds.feature): Added feature tests covering:
    • Successful funds release (updates status -> 'completed', balance -> 0.0).
    • Missing parameters (returns HTTP 400).
    • Escrow not found (returns HTTP 404).

How Has This Been Tested?

The Karate integration test suite was run in Docker Compose:

docker compose -f docker-compose-test.yml run --rm --build karate

Closes #418

Summary by CodeRabbit

  • New Features

    • Added escrow release support through a new release-funds endpoint, including success, validation, and not-found handling.
    • Added user and wallet sync endpoints for authentication-related updates.
    • Expanded bid-request routing to support both create and update actions.
    • Added reconciliation routing for escrow syncing.
  • Bug Fixes

    • Improved metadata table handling so schema details are recognized correctly instead of assuming defaults.
    • Updated webhook authentication wiring for more consistent routing behavior.
  • Tests

    • Added integration coverage for escrow release scenarios.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 78c1e6ff-4761-4f60-9156-4ec9491dd4b6

📥 Commits

Reviewing files that changed from the base of the PR and between 5659c0e and cb01bb2.

📒 Files selected for processing (1)
  • webhook/src/routes/index.js

📝 Walkthrough

Walkthrough

Adds a POST /api/escrows/release-funds webhook handler with Hasura GraphQL mutation fallback and Karate tests, new auth sync-user/sync-wallet routes, a reconciliation router, a bid-requests create route, a middleware export alias, a deploy-tenant schema-detection fix, and CI test infra yq installation.

Changes

Webhook routes and test infra

Layer / File(s) Summary
Auth middleware alias and import fix
webhook/src/middleware/auth.middleware.js, webhook/src/index.js, webhook/src/routes/index.js
authenticateFirebase is exported as an alias for authMiddleware, and imports are updated to reference the .middleware module path.
Release-funds handler, route, and wiring
webhook/src/routes/escrows/release-funds.handler.js, webhook/src/routes/escrows/release-funds.route.js, webhook/src/routes/index.js
Validates contractId/releaseSigner, attempts a camelCase Hasura mutation with a snake_case fallback, returns 200/404/500 accordingly, and mounts the route in the main router.
Release-funds Karate tests
tests/karate/features/escrows/release-funds.feature
Adds success, missing-field validation, and not-found scenarios against the seeded escrow fixture.
Auth sync-user and sync-wallet routes
webhook/src/routes/auth/index.js, webhook/src/routes/auth/sync-user.handler.js, webhook/src/routes/auth/sync-user.route.js, webhook/src/routes/auth/sync-wallet.route.js
Adds a runtime-typed upsert handler for public.users (uuid vs text id), and wires sync-user/sync-wallet sub-routes into the auth router.
Reconciliation router and bid-requests create route
webhook/src/routes/reconciliation/index.js, webhook/src/routes/bid-requests/index.js
Adds the reconciliation router mounting sync-escrows.route, and mounts a new create.route in bid-requests.
deploy-tenant schema detection and CI yq install
metadata/deploy-tenant.sh, docker-compose-test.yml
Table schema is now read from YAML instead of hard-coded public, and the CI compose file installs an architecture-matched yq binary and runs tenant setup.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TrustlessWork
  participant releaseFundsRoute
  participant releaseFundsHandler
  participant Hasura
  TrustlessWork->>releaseFundsRoute: POST /api/escrows/release-funds
  releaseFundsRoute->>releaseFundsHandler: forward request
  releaseFundsHandler->>releaseFundsHandler: validate contractId & releaseSigner
  releaseFundsHandler->>Hasura: camelCase mutation (update status=completed, balance=0)
  alt Hasura returns errors
    releaseFundsHandler->>Hasura: snake_case fallback mutation
  end
  Hasura-->>releaseFundsHandler: affected_rows
  alt no rows updated
    releaseFundsHandler-->>TrustlessWork: 404 escrow not found
  else success
    releaseFundsHandler-->>TrustlessWork: 200 {received: true}
  end
Loading

Possibly related PRs

Suggested reviewers: sotoJ24

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is related to the main change: adding escrow release-funds support.
Docstring Coverage ✅ Passed Docstring coverage is 100.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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

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

@drips-wave

drips-wave Bot commented Jun 27, 2026

Copy link
Copy Markdown

@KevinMB0220 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/karate/features/escrows/release-funds.feature (1)

9-38: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a mismatched-releaseSigner scenario.

The fixture seeds a releaser, but the suite never asserts that a different signer is rejected. That would have caught the current handler behavior where releaseSigner is ignored during the update.

🤖 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 `@tests/karate/features/escrows/release-funds.feature` around lines 9 - 38, Add
a negative test in the release-funds Karate feature for a mismatched
releaseSigner. Extend the existing scenarios in release-funds.feature by calling
the /api/escrows/release-funds endpoint with the correct contractId but an
unauthorized signer, then assert the request is rejected with the expected
error/status. Use the existing releaseSigner, contractId, and db.query checks as
reference points, and make sure the new scenario verifies the escrow row is not
marked completed and the balance is not updated when the signer does not match.
🤖 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 `@docker-compose-test.yml`:
- Around line 152-157: The test setup step that downloads yq should not use the
moving latest release and should fail immediately on bad downloads. In the
docker-compose test command near the yq install and metadata setup flow, pin the
yq URL to a specific release version, switch the download to curl -fsSL so HTTP
and write errors stop the script, and add a yq --version verification before
setup-tenant.sh is invoked to ensure the binary is valid.

In `@webhook/src/routes/bid-requests/index.js`:
- Around line 7-8: The new POST route wiring in the bid-requests router exposes
a contract mismatch between `createRoute` and the Karate feature expectations.
Update `create.handler` (and any related response shaping) so it accepts the
same request field names the `/api/bid-requests` consumer sends, returns the
response shape the tests assert, and matches the expected 400/409 message text;
use the `createRoute` and `create.handler` symbols to locate the request parsing
and response body logic.

In `@webhook/src/routes/escrows/release-funds.handler.js`:
- Around line 2-39: The release-funds handler currently validates releaseSigner
but does not use it, so the escrow can be completed using only contractId.
Update the lookup logic in release-funds.handler.js to include releaseSigner in
both mutation paths used by the handler (the custom camelCase mutation and the
default snake_case mutation), or add an equivalent verification step before the
admin-secret mutation is sent. Make sure the existing request body handling
around contractId and releaseSigner is preserved and that the Hasura where
clauses or callback check uniquely match both fields.

In `@webhook/src/routes/escrows/release-funds.route.js`:
- Around line 4-5: The release-funds endpoint is mounted outside the secured API
middleware chain, so it bypasses the normal auth/audit/limiting protections.
Update the route wiring for releaseFundsHandler in release-funds.route.js and
its mount in routes/index.js so it is either placed under the existing /api
chain or wrapped with the webhook-specific middleware stack (verifyAdminSecret,
validateJWT, auditLog, createTenantLimiter) at the mount point. Make sure the
final router composition keeps this state-changing endpoint protected
consistently with the other API routes.

In `@webhook/src/routes/index.js`:
- Around line 13-14: The release-funds webhook is currently mounted before
authenticateFirebase, so releaseFundsRoute can be reached without request
verification. Update the route setup in the router that mounts releaseFundsRoute
to apply webhook authentication/signature or shared-secret verification before
the handler runs, and ensure the /api/escrows/release-funds path is protected at
the route level rather than relying only on the Hasura admin-secret mutation.

---

Nitpick comments:
In `@tests/karate/features/escrows/release-funds.feature`:
- Around line 9-38: Add a negative test in the release-funds Karate feature for
a mismatched releaseSigner. Extend the existing scenarios in
release-funds.feature by calling the /api/escrows/release-funds endpoint with
the correct contractId but an unauthorized signer, then assert the request is
rejected with the expected error/status. Use the existing releaseSigner,
contractId, and db.query checks as reference points, and make sure the new
scenario verifies the escrow row is not marked completed and the balance is not
updated when the signer does not match.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec7a4637-77c8-48ff-8cfd-c2ab5f75f197

📥 Commits

Reviewing files that changed from the base of the PR and between 26df099 and 5659c0e.

📒 Files selected for processing (15)
  • docker-compose-test.yml
  • metadata/deploy-tenant.sh
  • tests/karate/features/escrows/release-funds.feature
  • tests/karate/fixtures/seed-test-escrows.sql
  • webhook/src/index.js
  • webhook/src/middleware/auth.middleware.js
  • webhook/src/routes/auth/index.js
  • webhook/src/routes/auth/sync-user.handler.js
  • webhook/src/routes/auth/sync-user.route.js
  • webhook/src/routes/auth/sync-wallet.route.js
  • webhook/src/routes/bid-requests/index.js
  • webhook/src/routes/escrows/release-funds.handler.js
  • webhook/src/routes/escrows/release-funds.route.js
  • webhook/src/routes/index.js
  • webhook/src/routes/reconciliation/index.js

Comment thread docker-compose-test.yml
Comment on lines +152 to +157
case \$(uname -m) in
aarch64|arm64) ARCH=arm64 ;;
*) ARCH=amd64 ;;
esac;
curl -L https://github.com/mikefarah/yq/releases/latest/download/yq_linux_\$ARCH -o /usr/local/bin/yq && chmod +x /usr/local/bin/yq;
cd metadata && ./setup-tenant.sh safetrust --endpoint http://graphql-engine-test:8080 --admin-secret myadminsecretkey

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## locate relevant files\n'
git ls-files | rg '^(docker-compose-test\.yml|metadata/(build-metadata\.sh|setup-tenant\.sh)|.*yq.*|README\.md)$'

printf '\n## line counts\n'
for f in docker-compose-test.yml metadata/build-metadata.sh metadata/setup-tenant.sh; do
  if [ -f "$f" ]; then
    wc -l "$f"
  fi
done

printf '\n## relevant excerpts\n'
sed -n '130,190p' docker-compose-test.yml
printf '\n--- metadata/build-metadata.sh ---\n'
sed -n '1,120p' metadata/build-metadata.sh
printf '\n--- metadata/setup-tenant.sh ---\n'
sed -n '1,200p' metadata/setup-tenant.sh

printf '\n## search for yq usage/version pinning\n'
rg -n "yq|curl -L https://github.com/mikefarah/yq|releases/latest|--version" -S .

Repository: safetrustcr/backend-SafeTrust

Length of output: 10719


Pin the yq download and fail fast.
releases/latest makes the test setup drift, and curl -L can still write a bad response to /usr/local/bin/yq; pin a specific release and use curl -fsSL plus a yq --version check before setup-tenant.sh runs.

🤖 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 `@docker-compose-test.yml` around lines 152 - 157, The test setup step that
downloads yq should not use the moving latest release and should fail
immediately on bad downloads. In the docker-compose test command near the yq
install and metadata setup flow, pin the yq URL to a specific release version,
switch the download to curl -fsSL so HTTP and write errors stop the script, and
add a yq --version verification before setup-tenant.sh is invoked to ensure the
binary is valid.

Comment on lines 7 to +8
router.use('/', updateRoute)
router.use('/', createRoute)

Copy link
Copy Markdown
Contributor

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

The new POST mount exposes a broken request/response contract.

webhook/src/routes/bid-requests/create.handler.js:19-66 reads camelCase fields (apartmentId, proposedPrice, desiredMoveIn) and returns { bidRequest }, but tests/karate/features/bid-requests/create-bid-request.feature:1-74 exercises /api/bid-requests with snake_case fields and asserts response.bid... plus different 400/409 messages. With Line 8 mounting this route, the advertised happy path will 400 on “missing required fields” instead of returning 201. Align the handler and consumer contract before merge.

🤖 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 `@webhook/src/routes/bid-requests/index.js` around lines 7 - 8, The new POST
route wiring in the bid-requests router exposes a contract mismatch between
`createRoute` and the Karate feature expectations. Update `create.handler` (and
any related response shaping) so it accepts the same request field names the
`/api/bid-requests` consumer sends, returns the response shape the tests assert,
and matches the expected 400/409 message text; use the `createRoute` and
`create.handler` symbols to locate the request parsing and response body logic.

Comment on lines +2 to +39
const { contractId, releaseSigner } = req.body;

if (!contractId || !releaseSigner) {
return res.status(400).json({
error: 'Missing required fields: contractId, releaseSigner'
});
}

// 1. Custom camelCase mutation (applied when metadata is fully loaded)
const mutationCustom = `
mutation ReleaseFunds($contractId: String!) {
update_trustlessWorkEscrows(
where: { contractId: { _eq: $contractId } }
_set: {
status: "completed",
balance: 0,
updatedAt: "now()"
}
) {
returning { id contractId status balance }
}
}
`;

// 2. Default snake_case mutation (fallback for default tracked tables)
const mutationDefault = `
mutation ReleaseFunds($contractId: String!) {
update_trustless_work_escrows(
where: { contract_id: { _eq: $contractId } }
_set: {
status: "completed",
balance: 0,
updated_at: "now()"
}
) {
returning { id contract_id status balance }
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Use releaseSigner in the escrow lookup.

The handler requires releaseSigner but never uses it, so a caller only needs a valid contractId to flip an escrow to completed. Please include the signer in both Hasura where clauses (or verify the callback signature before issuing the admin-secret mutation).

🤖 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 `@webhook/src/routes/escrows/release-funds.handler.js` around lines 2 - 39, The
release-funds handler currently validates releaseSigner but does not use it, so
the escrow can be completed using only contractId. Update the lookup logic in
release-funds.handler.js to include releaseSigner in both mutation paths used by
the handler (the custom camelCase mutation and the default snake_case mutation),
or add an equivalent verification step before the admin-secret mutation is sent.
Make sure the existing request body handling around contractId and releaseSigner
is preserved and that the Hasura where clauses or callback check uniquely match
both fields.

Comment on lines +4 to +5
const router = express.Router();
router.post('/api/escrows/release-funds', releaseFundsHandler);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

This route bypasses the /api auth middleware chain.

webhook/src/routes/index.js:10-19 mounts releaseFundsRoute before router.use('/api', authenticateFirebase), so this state-changing endpoint does not inherit the normal API protection. Please move it under the secured chain or attach the webhook-specific secret/JWT middleware at the mount point. Based on learnings, webhook routes should inherit the centralized verifyAdminSecret/validateJWT/auditLog/createTenantLimiter chain at router mounting.

🤖 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 `@webhook/src/routes/escrows/release-funds.route.js` around lines 4 - 5, The
release-funds endpoint is mounted outside the secured API middleware chain, so
it bypasses the normal auth/audit/limiting protections. Update the route wiring
for releaseFundsHandler in release-funds.route.js and its mount in
routes/index.js so it is either placed under the existing /api chain or wrapped
with the webhook-specific middleware stack (verifyAdminSecret, validateJWT,
auditLog, createTenantLimiter) at the mount point. Make sure the final router
composition keeps this state-changing endpoint protected consistently with the
other API routes.

Source: Learnings

Comment on lines +13 to 14
router.use(releaseFundsRoute)
router.use('/api', authenticateFirebase)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in webhook/src/routes/escrows/release-funds.route.js webhook/src/routes/escrows/release-funds.handler.js; do
  echo "=== $f ==="
  sed -n '1,220p' "$f"
  echo
done

echo "=== auth / signature checks around the escrow webhook ==="
rg -n -C2 'authenticateFirebase|authMiddleware|Authorization|signature|secret|hmac|verify' webhook/src/routes/escrows webhook/src/middleware

Repository: safetrustcr/backend-SafeTrust

Length of output: 10174


Protect /api/escrows/release-funds with request authentication
releaseFundsRoute is mounted before authenticateFirebase, and this handler has no signature/shared-secret check of its own. As written, anyone who can reach the endpoint can mark an escrow as completed via the Hasura admin secret. Add route-level webhook verification before the mutation.

🤖 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 `@webhook/src/routes/index.js` around lines 13 - 14, The release-funds webhook
is currently mounted before authenticateFirebase, so releaseFundsRoute can be
reached without request verification. Update the route setup in the router that
mounts releaseFundsRoute to apply webhook authentication/signature or
shared-secret verification before the handler runs, and ensure the
/api/escrows/release-funds path is protected at the route level rather than
relying only on the Hasura admin-secret mutation.

Source: Learnings

@sotoJ24 sotoJ24 self-requested a review July 1, 2026 03:47

@sotoJ24 sotoJ24 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dear @KevinMB0220, could you please update you're branch, everything else, well done, many thanks

@sotoJ24 sotoJ24 merged commit 639937d into safetrustcr:main Jul 2, 2026
1 check was pending
@grantfox-oss grantfox-oss Bot mentioned this pull request Jul 2, 2026
6 tasks
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.

feat: webhook release Funds Handler

2 participants