Skip to content

Conversation

@ajag408
Copy link
Contributor

@ajag408 ajag408 commented Jan 9, 2026

Summary

This PR adds a JSON-based interface and CLI to Shield, enabling validation from any programming language (Python, Go, Ruby, Rust, etc.) without requiring a native Shield implementation in each language.

Motivation

Shield is written in TypeScript, which limits its use to JavaScript/TypeScript applications. However, many Yield.xyz integrators use other languages for their backends. Rather than maintaining multiple implementations (which risks inconsistencies and security gaps), this PR provides a universal interface via stdin/stdout JSON.

What's Added

New Files

File Purpose
src/json/types.ts TypeScript types for JSON request/response
src/json/schema.ts Ajv JSON schema for strict input validation
src/json/handler.ts Main request handler (parse → validate → route → respond)
src/json/handler.test.ts Comprehensive test suite (23 tests)
src/json/index.ts Public exports
src/cli.ts CLI entry point (reads stdin, writes stdout)

Updated Files

File Changes
README.md Added documentation for cross-language usage with examples
package.json Added bin field for CLI, added ajv dependency
rslib.config.ts Added CLI build configuration

JSON Protocol

Request Format

{
  "apiVersion": "1.0",
  "operation": "validate" | "isSupported" | "getSupportedYieldIds",
  "yieldId": "ethereum-eth-lido-staking",
  "unsignedTransaction": "{...}",
  "userAddress": "0x..."
}

Response Format

{
  "ok": true,
  "apiVersion": "1.0",
  "result": { "isValid": true, "detectedType": "STAKE" },
  "meta": { "requestHash": "sha256..." }
}

Security Features

  • Input size limit: 100KB max to prevent DoS
  • Strict schema validation: Unknown properties rejected (no injection)
  • String length limits: All string fields have max lengths
  • Pre-compiled Ajv schema: Prevents ReDoS on repeated calls
  • Fail-closed design: Any parsing/validation error returns ok: false
  • No network access: Pure computation, deterministic output

Usage Examples

Bash

echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | npx @yieldxyz/shield

Python

result = subprocess.run(
    ["npx", "@yieldxyz/shield"],
    input=json.dumps(request),
    capture_output=True,
    text=True
)
response = json.loads(result.stdout)

Go

cmd := exec.Command("npx", "@yieldxyz/shield")
cmd.Stdin = bytes.NewReader(input)
output, _ := cmd.Output()

Testing

# Run JSON handler tests
pnpm jest src/json/handler.test.ts

# Test CLI manually after build
pnpm build
echo '{"apiVersion":"1.0","operation":"getSupportedYieldIds"}' | node dist/cli.js

Test Coverage

  • ✅ Valid transaction validation (stake, unstake)
  • ✅ Invalid transaction rejection (wrong contract, wrong referral, wrong from address)
  • ✅ Missing required fields
  • ✅ Schema validation (invalid JSON, unknown properties, oversized input)
  • ✅ Tampering detection (appended data, modified selector, truncated data)
  • ✅ Response integrity (requestHash consistency)

Breaking Changes

None. This is purely additive.

Checklist

  • All tests pass (pnpm test)
  • Build succeeds (pnpm build)
  • CLI works after build
  • README updated with usage examples
  • No new lint warnings

Copilot AI review requested due to automatic review settings January 9, 2026 06:37
@socket-security
Copy link

socket-security bot commented Jan 9, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedajv@​8.17.19910010082100

View full report

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a JSON-based interface and CLI to Shield, enabling cross-language validation of transactions from any programming language (Python, Go, Ruby, Rust, etc.) without requiring native Shield implementations. The change introduces a standardized JSON protocol for stdin/stdout communication, comprehensive input validation using Ajv JSON Schema, and security features including input size limits, strict schema validation, and fail-closed error handling.

Key changes:

  • JSON Interface: New JSON request/response types, Ajv-based schema validation, and request handler with operation routing
  • CLI Tool: Command-line interface that reads JSON from stdin and writes responses to stdout
  • Documentation: Comprehensive README updates with examples for Bash, Python, Go, and Ruby

Reviewed changes

Copilot reviewed 10 out of 13 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/json/types.ts TypeScript type definitions for JSON requests, responses, and error codes
src/json/schema.ts Ajv JSON schema with security constraints and operation-specific field requirements
src/json/handler.ts Main request handler with parsing, validation, routing, and response formatting
src/json/handler.test.ts Comprehensive test suite covering validation, security, and error cases
src/json/index.ts Public exports for JSON interface types and functions
src/cli.ts CLI entry point that reads stdin and invokes the JSON handler
src/index.ts Updated exports to include JSON interface functions and types
rslib.config.ts Added CLI build configuration for CommonJS output
package.json Added bin field for CLI executable and ajv dependency
pnpm-lock.yaml Lockfile updates for ajv 8.17.1 and its dependencies
README.md Added cross-language usage documentation with CLI and language-specific examples
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +30 to +51
args: {
type: 'object',
additionalProperties: false, // Security: reject unknown fields
properties: {
// Currently used by Tron
validatorAddress: { type: 'string', maxLength: 128 },
validatorAddresses: {
type: 'array',
items: { type: 'string', maxLength: 128 },
maxItems: 100,
},

// Future use - include for forward compatibility
amount: { type: 'string', maxLength: 78 }, // Max uint256 is 78 digits
tronResource: { type: 'string', enum: ['BANDWIDTH', 'ENERGY'] },
providerId: { type: 'string', maxLength: 256 },
duration: { type: 'number', minimum: 0 },
inputToken: { type: 'string', maxLength: 128 },
subnetId: { type: 'number', minimum: 0 },
feeConfigurationId: { type: 'string', maxLength: 256 },
cosmosPubKey: { type: 'string', maxLength: 256 },
tezosPubKey: { type: 'string', maxLength: 256 },
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Inconsistent indentation in the schema definition. The args object properties are indented with 4 spaces while the parent level uses 2 spaces. This makes the code harder to read and maintain. Consider using consistent indentation (2 spaces) throughout the schema definition.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +79
},
},
},
},
},
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The operationRequirements object is not type-safe. The keys are not constrained to match the operation enum values, and there's no compile-time guarantee that all operations are covered. Consider using a Record type with the operation type as the key, or use a const assertion with 'as const' and 'satisfies' to ensure type safety.

Copilot uses AI. Check for mistakes.
Comment on lines +149 to +150
return json.loads(result.stdout)

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The Python example doesn't check for subprocess errors or handle the case where the subprocess fails. If the subprocess fails (e.g., if npx is not installed), result.stdout will be empty and json.loads will raise an exception. Consider adding error handling, such as checking result.returncode or wrapping the json.loads call in a try-except block.

Suggested change
return json.loads(result.stdout)
if result.returncode != 0:
error_msg = result.stderr.strip() or "Shield CLI failed with a non-zero exit code."
raise RuntimeError(f"Failed to validate transaction: {error_msg}")
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
raise ValueError("Shield CLI returned invalid or empty JSON.") from e

Copilot uses AI. Check for mistakes.
Comment on lines +158 to +161
if response["ok"] and response["result"]["isValid"]:
print("Transaction is valid!")
else:
print(f"Blocked: {response['result'].get('reason')}")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The Python example doesn't properly handle error responses where 'result' key may not exist (when ok is false, the response contains an 'error' key instead of 'result'). This will raise a KeyError. Consider checking response['ok'] first and accessing the appropriate key, or using response.get('result', {}).get('reason') for safer access.

Suggested change
if response["ok"] and response["result"]["isValid"]:
print("Transaction is valid!")
else:
print(f"Blocked: {response['result'].get('reason')}")
if response.get("ok") and response.get("result", {}).get("isValid"):
print("Transaction is valid!")
else:
reason = response.get("result", {}).get("reason") or response.get("error") or "Unknown error"
print(f"Blocked: {reason}")

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +5

const MAX_INPUT_SIZE = 100 * 1024; // 100KB

Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The MAX_INPUT_SIZE constant is duplicated between cli.ts and handler.ts. This creates a maintenance burden and risk of inconsistency. Consider exporting the constant from handler.ts and importing it in cli.ts, or defining it in a shared constants file to ensure both use the same limit.

Suggested change
const MAX_INPUT_SIZE = 100 * 1024; // 100KB
import { MAX_INPUT_SIZE } from './handler';

Copilot uses AI. Check for mistakes.
"userAddress": userAddress,
}

input, _ := json.Marshal(request)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The Go example silently ignores the error from json.Marshal by using the blank identifier. While this is unlikely to fail, it's better to handle the error explicitly for completeness and to demonstrate proper error handling in example code.

Suggested change
input, _ := json.Marshal(request)
input, err := json.Marshal(request)
if err != nil {
return false, err
}

Copilot uses AI. Check for mistakes.
IsValid bool `json:"isValid"`
} `json:"result"`
}
json.Unmarshal(output, &resp)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The Go example doesn't check the error return value from json.Unmarshal. If the output is invalid JSON or the structure doesn't match, the resp struct will have zero values and the function will incorrectly return false, nil instead of an error. Consider checking and returning the error from json.Unmarshal.

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +222
stdout, _status = Open3.capture2("npx @yieldxyz/shield", stdin_data: request.to_json)
JSON.parse(stdout)
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The Ruby example doesn't check the status of the command execution. If the subprocess fails, stdout might be empty or contain an error, and JSON.parse could raise an exception. Consider checking _status and handling potential JSON parsing errors.

Suggested change
stdout, _status = Open3.capture2("npx @yieldxyz/shield", stdin_data: request.to_json)
JSON.parse(stdout)
stdout, status = Open3.capture2("npx @yieldxyz/shield", stdin_data: request.to_json)
unless status.success?
raise "shield command failed with status #{status.exitstatus}"
end
begin
JSON.parse(stdout)
rescue JSON::ParserError => e
raise "failed to parse shield output as JSON: #{e.message}"
end

Copilot uses AI. Check for mistakes.
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.

2 participants