-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add language-agnostic JSON interface and CLI for cross-language Shield usage #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: Add language-agnostic JSON interface and CLI for cross-language Shield usage #2
Conversation
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this 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.
| 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 }, |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| return json.loads(result.stdout) | ||
|
|
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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 |
| if response["ok"] and response["result"]["isValid"]: | ||
| print("Transaction is valid!") | ||
| else: | ||
| print(f"Blocked: {response['result'].get('reason')}") |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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}") |
|
|
||
| const MAX_INPUT_SIZE = 100 * 1024; // 100KB | ||
|
|
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| const MAX_INPUT_SIZE = 100 * 1024; // 100KB | |
| import { MAX_INPUT_SIZE } from './handler'; |
| "userAddress": userAddress, | ||
| } | ||
|
|
||
| input, _ := json.Marshal(request) |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| input, _ := json.Marshal(request) | |
| input, err := json.Marshal(request) | |
| if err != nil { | |
| return false, err | |
| } |
| IsValid bool `json:"isValid"` | ||
| } `json:"result"` | ||
| } | ||
| json.Unmarshal(output, &resp) |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| stdout, _status = Open3.capture2("npx @yieldxyz/shield", stdin_data: request.to_json) | ||
| JSON.parse(stdout) |
Copilot
AI
Jan 9, 2026
There was a problem hiding this comment.
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.
| 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 |
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
src/json/types.tssrc/json/schema.tssrc/json/handler.tssrc/json/handler.test.tssrc/json/index.tssrc/cli.tsUpdated Files
README.mdpackage.jsonbinfield for CLI, addedajvdependencyrslib.config.tsJSON 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
ok: falseUsage Examples
Bash
Python
Go
Testing
Test Coverage
Breaking Changes
None. This is purely additive.
Checklist
pnpm test)pnpm build)