7 Commits

Author SHA1 Message Date
a35dc508b0 client: allow injecting an http(s).Agent + reject redirects
All checks were successful
Build and Test / build (18.x) (push) Successful in 50s
Build and Test / build (20.x) (push) Successful in 47s
Build and Test / build (16.x) (push) Successful in 54s
The HTTP client built its connections with no injectable agent, so a
server-side caller pointing the client at an attacker-influenced baseURL
(a tenant's prime_endpoint) had no way to attach an SSRF policy at connect
time. node's http.request doesn't follow redirects, but a 3xx was treated
as success and its body mis-parsed.

- ClientConfig accepts an optional `agent`; inject one whose connection
  factory refuses internal IPs (incl. DNS-rebinding defense) when the
  baseURL is untrusted. Default stays unguarded for trusted/CLI use — the
  guard belongs in the server.
- A 3xx response is now an explicit error ("refusing to follow"), so a
  redirect can't be silently mis-handled or, via a future change, followed
  to an internal host.
2026-06-04 12:41:35 -04:00
0f34eb3e41 release: v1.2.0 (getInterchain trace methods)
All checks were successful
Build and Test / build (20.x) (push) Successful in 50s
Publish to NPM Registry / publish (release) Successful in 48s
Build and Test / build (16.x) (push) Successful in 58s
Build and Test / build (18.x) (push) Successful in 53s
2026-06-02 15:19:11 -04:00
2ffa63df41 add getInterchain: trace a transaction/block to validator blocks + interchain anchors
All checks were successful
Build and Test / build (20.x) (push) Successful in 45s
Build and Test / build (18.x) (push) Successful in 48s
Build and Test / build (16.x) (push) Successful in 52s
New transaction.getInterchain and block.getInterchain call the prime-node
/api/v1/{transaction,block}/{id}/interchain endpoints, returning an
InterchainTrace { blockId, validatorBlocks, interchainTransactions }. Adds
VerificationBlock / InterchainTransaction / InterchainTrace interfaces and
method-existence assertions.
2026-06-02 14:12:59 -04:00
e4430ae27c Fix Block model to match server: nest header with camelCase keys
All checks were successful
Build and Test / build (20.x) (push) Successful in 2m14s
Build and Test / build (18.x) (push) Successful in 2m21s
Build and Test / build (16.x) (push) Successful in 2m24s
The block endpoint returns block id / prev / timestamp nested under a
"header" object with camelCase keys (blockId, dcId, prevId, prevProof,
timestamp) and a proof of just {proof}. The previous flat snake_case
Block fields never matched the response and always deserialized empty.
Add a BlockHeader interface, nest it in Block, and make proof.scheme
optional. Verified live against a dev chain.
2026-05-29 17:08:41 -04:00
2071db93e5 Fix publish auth: use SA_DC_BUILD_API_TOKEN org secret
All checks were successful
Build and Test / build (16.x) (push) Successful in 15s
Build and Test / build (18.x) (push) Successful in 14s
Build and Test / build (20.x) (push) Successful in 13s
2026-03-17 11:10:22 -04:00
b6019a48e5 Fix publish workflow: configure Gitea registry only for publish step
All checks were successful
Build and Test / build (16.x) (push) Successful in 15s
Build and Test / build (18.x) (push) Successful in 13s
Build and Test / build (20.x) (push) Successful in 14s
2026-03-17 11:08:43 -04:00
14ac869ed6 Add workflow_dispatch and published trigger to publish workflow
All checks were successful
Build and Test / build (20.x) (push) Successful in 13s
Build and Test / build (18.x) (push) Successful in 13s
Build and Test / build (16.x) (push) Successful in 16s
2026-03-17 11:06:53 -04:00
8 changed files with 108 additions and 13 deletions

View File

@@ -2,7 +2,8 @@ name: Publish to NPM Registry
on:
release:
types: [created]
types: [created, published]
workflow_dispatch:
jobs:
publish:
@@ -16,7 +17,6 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: '20.x'
registry-url: 'https://git.dragonchain.com/api/packages/dragonchain/npm/'
- name: Install dependencies
run: npm ci
@@ -28,6 +28,9 @@ jobs:
run: npm run build
- name: Publish to Gitea NPM registry
run: npm publish
run: |
echo "@dragonchain-inc:registry=https://git.dragonchain.com/api/packages/dragonchain/npm/" > ~/.npmrc
echo "//git.dragonchain.com/api/packages/dragonchain/npm/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc
npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITEA_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.SA_DC_BUILD_API_TOKEN }}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@dragonchain-inc/prime-sdk",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@dragonchain-inc/prime-sdk",
"version": "1.1.0",
"version": "1.2.0",
"license": "Apache-2.0",
"dependencies": {
"js-yaml": "^4.1.0"

View File

@@ -1,6 +1,6 @@
{
"name": "@dragonchain-inc/prime-sdk",
"version": "1.1.0",
"version": "1.2.0",
"description": "Official Dragonchain Prime SDK for Node.js and TypeScript",
"main": "./dist/index.js",
"module": "./dist/index.mjs",

View File

@@ -3,7 +3,7 @@
*/
import { DragonchainClient } from './client';
import { Block } from './types';
import { Block, InterchainTrace } from './types';
export class BlockClient {
private client: DragonchainClient;
@@ -18,4 +18,12 @@ export class BlockClient {
async get(blockId: string): Promise<Block> {
return this.client.get<Block>(`/api/v1/block/${blockId}`);
}
/**
* Traces a block to the validator (verification) blocks that validated it and
* the public-chain interchain anchors those validator blocks were bundled into.
*/
async getInterchain(blockId: string): Promise<InterchainTrace> {
return this.client.get<InterchainTrace>(`/api/v1/block/${blockId}/interchain`);
}
}

View File

@@ -13,6 +13,15 @@ export interface ClientConfig {
authKey: string;
baseURL: string;
timeout?: number;
/**
* Optional http(s).Agent used for every request. Inject an agent whose
* connection factory refuses to connect to internal IPs when baseURL is
* attacker-influenced (a tenant's prime_endpoint), to defend against SSRF
* (incl. DNS rebinding) at connect time. The SSRF policy belongs in the
* server that points this client at untrusted endpoints, not baked into
* the client library, so the default (no agent) is unguarded.
*/
agent?: http.Agent | https.Agent;
}
export class DragonchainClient {
@@ -21,6 +30,7 @@ export class DragonchainClient {
private readonly authKey: string;
private readonly baseURL: string;
private readonly timeout: number;
private readonly agent?: http.Agent | https.Agent;
constructor(config: ClientConfig) {
this.publicId = config.publicId;
@@ -28,6 +38,7 @@ export class DragonchainClient {
this.authKey = config.authKey;
this.baseURL = config.baseURL.replace(/\/$/, ''); // Remove trailing slash
this.timeout = config.timeout || 30000; // Default 30 seconds
this.agent = config.agent;
}
/**
@@ -123,6 +134,7 @@ export class DragonchainClient {
'Content-Length': bodyBuffer.length,
},
timeout: this.timeout,
...(this.agent && { agent: this.agent }),
};
return new Promise<T>((resolve, reject) => {
@@ -137,6 +149,19 @@ export class DragonchainClient {
res.on('end', () => {
const responseBody = Buffer.concat(chunks);
// Refuse redirects. node's http.request does not follow them, but
// treating a 3xx as success would mis-parse the (empty) body — and
// a followed redirect to an internal host would be an SSRF vector.
// Prime never legitimately redirects, so a 3xx is an error.
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {
reject(
new Error(
`Unexpected redirect (status ${res.statusCode}) to ${res.headers.location ?? '?'}; refusing to follow`
)
);
return;
}
// Check for errors
if (res.statusCode && res.statusCode >= 400) {
const errorMessage = responseBody.toString('utf8').trim();

View File

@@ -11,6 +11,7 @@ import {
TransactionBulkResponse,
Transaction,
ListTransactionsResponse,
InterchainTrace,
} from './types';
export class TransactionClient {
@@ -49,6 +50,16 @@ export class TransactionClient {
return this.client.get<Transaction>(`/api/v1/transaction/${transactionId}`);
}
/**
* Traces a transaction to the validator (verification) blocks that validated
* its prime block and the public-chain interchain anchors those validator
* blocks were bundled into. If the transaction is still pending (not yet in a
* block) the trace's arrays are empty.
*/
async getInterchain(transactionId: string): Promise<InterchainTrace> {
return this.client.get<InterchainTrace>(`/api/v1/transaction/${transactionId}/interchain`);
}
/**
* Lists all transactions
*/

View File

@@ -122,21 +122,67 @@ export interface SmartContract {
// Block Types
export interface BlockProof {
scheme: string;
proof: string;
scheme?: string; // absent on trust-scheme chains; present on PoW
nonce?: number;
}
export interface BlockHeader {
blockId: string;
dcId: string;
prevId: string;
prevProof: string;
timestamp: string;
}
export interface Block {
version: string;
block_id: string;
timestamp: string;
prev_id: string;
prev_proof: string;
header: BlockHeader;
transactions: string[];
proof: BlockProof;
}
// Interchain Trace Types
/** A validator's verification of a prime block. */
export interface VerificationBlock {
version: string;
primeChainId: string;
primeBlockId: string;
timestamp: string;
verifierPublicKey: string;
verifierSignature: string;
}
/**
* An anchor broadcast to a public blockchain (e.g. ETH/BTC) bundling one or more
* validator blocks. `validatorBlocks` holds the covered prime block ids;
* `coveredPrimeChainIds` the prime chains they belong to.
*/
export interface InterchainTransaction {
id: number;
version: string;
timestamp: string;
chainId: string;
transHash: string;
blockId: string;
validatorBlocks: string[];
validatorBlockhash: string;
signature: string;
coveredPrimeChainIds: string[];
}
/**
* Links a prime block to the validator (verification) blocks that validated it
* and the public-chain interchain anchors those validator blocks were bundled
* into. Returned by `transaction.getInterchain` and `block.getInterchain`.
*/
export interface InterchainTrace {
blockId: string;
validatorBlocks: VerificationBlock[];
interchainTransactions: InterchainTransaction[];
}
// System Types
export interface SystemStatus {
id: string;

View File

@@ -47,6 +47,7 @@ describe('DragonchainSDK', () => {
expect(typeof sdk.transaction.create).toBe('function');
expect(typeof sdk.transaction.createBulk).toBe('function');
expect(typeof sdk.transaction.get).toBe('function');
expect(typeof sdk.transaction.getInterchain).toBe('function');
expect(typeof sdk.transaction.list).toBe('function');
});
@@ -68,6 +69,7 @@ describe('DragonchainSDK', () => {
it('should have block module with correct methods', () => {
expect(typeof sdk.block.get).toBe('function');
expect(typeof sdk.block.getInterchain).toBe('function');
});
it('should have system module with correct methods', () => {