@debridge-finance/borderless-validation-sdk
Validation SDK for deBridge borderless intents API (POST /v1.0/bundle) responses.
Purpose
When a user submits a bundle request to the deBridge API, the response contains transactions and signable payloads (EIP-712 messages, permits, Solana transactions, etc.) that the user is expected to sign.
Every field the user signs is a potential attack vector. If the API is compromised (MITM, server bug, malicious rewrite), the user could unknowingly sign a transaction that drains their funds.
This SDK runs on the client before signing. It validates the response structure and checks signable payloads against the original request, protocol invariants, and environment-specific whitelists.
Threats mitigated
- MITM attacks against the API endpoint.
- Bugs on the API side that produce subtly wrong responses.
- Reduced API trust — the client no longer has to take API responses on faith.
- Regression safety net when testing new API versions.
Installation
npm install @debridge-finance/borderless-validation-sdk
Declared peer dependency ranges:
@solana/web3.js >= 1.90.0bs58 >= 5.0.0viem >= 2.0.0zod ^3.0.0 || ^4.0.0
Basic usage
import {
createValidator,
Environment,
ViolationCode,
} from "@debridge-finance/borderless-validation-sdk";
const validator = createValidator({ environment: Environment.Prod });
const url = "https://cross-chain-api.debridge.finance/v1.0/bundle";
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
}).then(res => res.json());
const result = validator.validate(url, request, response);
if (!result.ok) {
if (result.violations.some(v => v.code === ViolationCode.HigherApiVersion)) {
// Special case: the API response was produced for a newer validation
// version than this SDK supports. The SDK cannot reliably validate it.
// Decide in your app whether to block, warn, update the SDK, or use
// another fallback flow.
console.error("API validation version is newer than SDK support:", result.violations);
return;
}
if (result.violations.some(v => v.code === ViolationCode.UnexpectedValidationError)) {
// Special case: this indicates an internal Validation SDK error.
// The client app must decide how to handle it: block signing, report
// telemetry, retry after SDK update, or use another fallback flow.
console.error("Validation SDK internal error:", result.violations);
return;
}
console.error("Validation failed:", result.violations);
// Do NOT let the user sign anything.
return;
}
// Safe to proceed with signing.
API
createValidator(config: ValidatorConfig): Validator
type ValidatorConfig = {
environment: Environment;
};
type Validator = {
validate(url: string, request: object, response: object): ValidationResult;
};
type ValidationResult = {
ok: boolean; // true if no violations
violations: Violation[]; // all issues found
};
type Violation = {
code: ViolationCode; // machine-readable identifier
message: string; // human-readable description
path: string; // JSON path of the offending field
expected?: unknown; // the value the validator expected (concrete, comparable to `actual`)
actual?: unknown; // the value found in the response (concrete, comparable to `expected`)
details?: unknown; // extra machine-readable context (e.g. caught error object)
};
validator.validate() never throws — every error condition is reported as a Violation. Safe to call in any code path.
Integration guide for UI teams
The SDK is designed to run between receiving the API response and presenting it to the user for signing. Wire it into that seam.
1. Where to call
┌───────────────┐ ┌─────────┐ ┌────────────────────┐ ┌───────────────┐
│ Build request │ ──▶ │ fetch() │ ──▶ │ validator.validate │ ──▶ │ Sign / submit │
└───────────────┘ └─────────┘ └────────────────────┘ └───────────────┘
│
▼
ok ? proceed : block
- Before
validate: the user has not yet seen or signed anything. - After
validate: ifresult.ok === false, do not forward any signable payload (intent message, permit, transaction) to the wallet. The API response cannot be trusted.
2. Choosing the environment
import { Environment } from "@debridge-finance/borderless-validation-sdk";
Environment.Prod— production without gasless features.Environment.Gasless— production with gasless features.Environment.Dev— developer environment (also known as Stage).
Pass the same environment your app is already configured for — don't make it user-selectable.
3. Pass the exact URL you called
const url = "https://cross-chain-api.debridge.finance/v1.0/bundle";
const response = await fetch(url, { ... });
const result = validator.validate(url, request, response);
The url argument is accepted for API compatibility and future endpoint checks. Pass the same string you handed to fetch; do not rely on URL validation until it is explicitly enabled in a release.
4. Handling violations
On result.ok === false:
- Block the signing flow. Do not show a wallet popup or write any pending tx to state.
- Log the full
violationsarray to your error reporting system (Sentry / Datadog / etc.). Includecode,path,expected,actual,details. - Show the user a generic error. Do not leak raw
Violationdata into the UI — messages are for developers, not end-users. A single "Quote is invalid, please retry" is enough. - Do not retry automatically. If the API is compromised, retries make it worse. Let the user manually trigger a retry.
5. Browser compatibility
The SDK is published as both CJS and ESM and uses only browser-compatible APIs (no fs, crypto, Buffer, process).
Tree-shaking: the ESM build preserves named exports. Importing only createValidator will not pull in unused validators.
6. Bundle size
Roughly 1 MB unpacked (a lot of it is source maps — drop them in production). The heaviest peer dep is @solana/web3.js; if your app only deals with EVM trades, your bundler's tree-shaking should eliminate most of it.
7. TypeScript
Full type declarations ship in dist/types/. ValidatorConfig, ValidationResult, Violation, and ViolationCode are all exported — use them directly, don't re-declare.
8. What NOT to do
- ❌ Don't skip validation because "the API is our own." Compromise detection is the whole point.
- ❌ Don't mutate
requestorresponsebetween the API call andvalidate()— the validator needs the exact payload the user received. - ❌ Don't interpret
violations[].messageprogrammatically — message wording may change between versions. Branch oncodeinstead. - ❌ Don't cache the validator if
environmentchanges. Create a new instance viacreateValidatorfor each environment.
Development
See CLAUDE.md for project conventions, validation rules, and security guidelines.
See src/README.md for an overview of the source code structure and validation flow.
- Unit tests:
npm test - Zod 3 unit compatibility tests:
npm run test:zod3 - Zod 3 E2E compatibility tests:
npm run test:e2e:zod3 - Zod 3 E2E compatibility tests (CI mode):
npm run test:e2e:zod3:ci - Zod 3/4 full compatibility checks:
npm run test:zod-compat - Unit test coverage:
npm run coverage - E2E tests (use the stage API by default; override with
API_BASE_URL):npm run test:e2e - E2E tests with validation-case recording:
npm run test:e2e:record-validation-cases - E2E tests (CI mode, serial with longer timeout):
npm run test:e2e:ci - E2E coverage:
npm run coverage:e2e - Download the matching Java validator CLI:
npm run download:java-validator-cli - Compare TypeScript and Java E2E validation results:
npm run test:e2e:java-parity - Full build:
npm run build - Build ESM only:
npm run build:esm - Build CJS only:
npm run build:cjs - Build type declarations only:
npm run build:types - Clean build output:
npm run clean
Releasing
Releases are cut by Nx Release from the conventional-commit PR titles:
- Title the PR as a conventional commit:
fix: ...(patch),feat: ...(minor),feat!: ...(major). Other types (ci,chore,docs,test,refactor, ...) do not trigger a release. - Squash-merge the PR into
master. - Actions → Create Release → Run workflow on
master; tickdry-runto preview the next version and changelog first. After the approval of thereleaseenvironment the workflow bumpspackage.json, updatesCHANGELOG.md, pushes thev<version>tag, creates the GitHub release and publishes the package to npm (trusted publishing, no npm token).
To release changes merged without a fix:/feat: title, set version (patch, minor, major or 1.2.3).
If only the npm publish step failed, re-run Create Release with publish_only.
Java parity E2E validation
This check runs the repository's E2E validation cases through both the TypeScript SDK and its Java port, then verifies that both implementations produce equivalent validation results.
1. Running the check
Use the TypeScript SDK version from the root package.json:
npm run test:e2e:java-parity
To select the Java release associated with another TypeScript SDK version, pass that version explicitly:
npm run test:e2e:java-parity [-- <typescript-sdk-version>]
The argument selects the Java release mapping only. It does not check out or install that version of the TypeScript SDK; TypeScript results always come from the current working tree.
The command requires Java and access to GitHub releases. For authenticated GitHub access, it uses
GITHUB_TOKEN, GH_TOKEN, or the token returned by gh auth token, in that order.
2. Execution flow and generated files
The command performs the following steps:
-
Determines the requested TypeScript SDK version from the command-line argument or the root
package.json. -
Finds the newest published Java release whose release notes contain
TypeScript SDK tag: v<version>and expects that release to contain exactly onecli-*.jarasset. -
Downloads and verifies the Java CLI, unless the same verified release is already cached under:
.cache/java-validation-sdk/releases/<typescript-version>/<java-release-tag>/This directory contains the CLI JAR and its
metadata.jsoncache metadata. -
Checks that the Java CLI supports the expected CLI contract version.
-
Runs the regular TypeScript E2E suite with validation-case recording enabled. Every Jest worker writes its validator inputs and TypeScript results to a separate JSONL file:
.cache/recorded-validation-cases/<date>__<unix-timestamp>/ └── recorded-cases__pid-<pid>.jsonlEach JSONL line contains the test identity, validator URL, request, response, validator config, and TypeScript
ValidationResultfor one validator invocation. -
Sends every recorded input to the Java CLI daemon in the same order and reads one Java
ValidationResultper input. -
Compares the corresponding TypeScript and Java results. Up to ten mismatches are printed to the console. The complete mismatch report is written to:
.cache/java-validation-sdk/parity-check-runs/<date>__<unix-timestamp>/mismatches.json
The temporary .cache/recorded-validation-cases/latest-run.json file connects the E2E recorder to
the parity runner and is removed when the command finishes. A successful parity check also removes
its recorded validation cases. If E2E execution or parity comparison fails, the recorded cases are
preserved for investigation. A parity-check run directory is created only when mismatches need to
be reported.
3. Comparison rules
The raw ValidationResult objects are not compared directly. Each result is reduced to this shape:
{
ok: boolean;
violations: Array<{
code: string;
path: string;
expected?: unknown;
actual?: unknown;
}>;
}
The comparison includes:
- the top-level
okvalue; - the number of violations, including duplicate violations;
- each violation's
codeandpath; - the presence and JSON value of
expectedandactual; - nested value types, string casing, and array element order inside
expectedandactual.
Before comparison:
- violations are sorted, so the order in which the two SDKs report violations does not matter;
- object keys are sorted recursively, so object property order does not matter;
- arrays are not sorted, so their element order remains significant.
The comparison intentionally ignores:
- violation
messagetext; - violation
details; - any other violation fields;
- any other top-level result fields.
After normalization, the two result objects are serialized and compared exactly. Any remaining difference is reported as a parity mismatch.
License
BUSL-1.1