🚀 Socket Launch Week Day 4:Socket MCP Adds Org Alerts, Threat Feed Review, and Package Inspection.Learn more
Sign In

@powforge/identity

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@powforge/identity - npm Package Compare versions

Comparing version
0.7.0-beta.1
to
0.7.0-beta.2
+45
-0
CHANGELOG.md
# Changelog
## 0.7.0-beta.2
### Added
- `getChaintip({ source })` now accepts an optional source config. Pass
`{ type: 'bitcoind-rpc', url, user, password }` to fetch the tip from a
self-hosted Bitcoin Core node via JSON-RPC (`getblockcount` +
`getbestblockhash`) instead of mempool.space. The returned envelope is
unchanged (`{ height, hash, observed_at }`), so downstream signature
verification is wire-compatible.
- 60s in-memory cache is now keyed per source, so callers using both
mempool.space and a local RPC in the same process do not poison each
other's cached values.
### Backwards compatibility
- Default source remains mempool.space. Consumers who
`npm install @powforge/identity` without configuring anything see
byte-identical behavior to 0.7.0-beta.1. The RPC mode is strictly
opt-in via `opts.source`.
### Consumers migrated in this repo
- `scripts/identity-score-server.js` loads creds from
`~/.config/powforge/bitcoin-rpc` at boot and threads the RPC source
through every `?chaintip=1` request. If the primary source fails, it
degrades to mempool.space before setting `bitcoin_tip_error`.
- `scripts/lib/bitcoin-pulse.js` auto-detects the creds file and prefers
local RPC for tip/fees/mempool. Fees come from `estimatesmartfee` at
N=1/3/6/144/504 blocks (fastest / halfHour / hour / economy / minimum),
converted BTC/kvB -> sat/vB with ceiling rounding. Graceful fallback
to mempool.space on RPC error with a single stderr warning per process.
- New helper: `scripts/lib/bitcoin-rpc-config.js` parses the creds file
once and returns either a source object ready for `getChaintip`/pulse
or null for the fallback path.
### Motivation
- Fubz directive msg 1510 (2026-04-23): "wire @powforge/identity chaintip
+ forge-tick bitcoin_pulse to use the internal Bitcoin node at
lightning.lan:8332." Local RPC means lower latency, no mempool.space
rate limit, and a stronger trust story ("oracle queries the operator's
own node, not a public API").
### Beta tag
- Published as `@beta` on npm so `latest` stays pinned to 0.6.0 for
existing callers. Consumers opt in via
`npm install @powforge/identity@beta`.
## 0.7.0-beta.1

@@ -4,0 +49,0 @@

+1
-1
{
"name": "@powforge/identity",
"version": "0.7.0-beta.1",
"version": "0.7.0-beta.2",
"description": "Depth-of-Identity SDK for Nostr. Measures accumulated irreversible work across five dimensions. Try it live at powforge.dev/explorer.",

@@ -5,0 +5,0 @@ "keywords": [

@@ -10,19 +10,21 @@ /**

* What this provides:
* - getChaintip() returns { height, hash, observed_at } from mempool.space
* - getChaintip() returns { height, hash, observed_at }
* Default source: mempool.space HTTP endpoints
* Optional source: a user-provided bitcoind JSON-RPC
* endpoint (see `opts.source`).
* - signWithChaintip() thin wrapper that adds bitcoin_tip under the signer's payload
* - 60-second in-memory cache to avoid hammering mempool.space on hot paths
* - 60-second in-memory cache to avoid hammering any single source on hot paths
*
* What this deliberately does NOT provide:
* - SPV verification, Merkle proofs, or header-chain validation
* - A self-hosted Bitcoin RPC fallback (see TODO below)
* - Automatic discovery of a bitcoind endpoint — callers thread `opts.source` in
*
* Backwards-compat contract:
* A consumer who does `npm install @powforge/identity` and calls
* `getChaintip()` with no args sees the same mempool.space path as before.
* The RPC mode is strictly opt-in via `opts.source`.
*
* Honest claim for downstream copy:
* "Block height + hash as a freshness anchor, verifiable against any
* public Bitcoin node."
*
* Upgrade path:
* TODO(bitcoind-rpc): When Fubz credentials arrive, swap the mempool.space
* HTTP fetch for a direct bitcoind JSON-RPC call (getblockchaininfo). The
* public API becomes `CHAINTIP_RPC_URL` env var plus auth, and the cache
* window can shrink because RPC latency drops an order of magnitude.
*/

@@ -36,29 +38,96 @@

let cached = null; // { value: {height, hash, observed_at}, expires_at: ms-epoch }
// Cache is keyed by the resolved source identity so that two callers with
// different sources (e.g. one using mempool.space, one using a local RPC) do
// not poison each other's cached value. Keyspace stays tiny in practice — a
// Node process realistically has one or two source identities.
const cacheByKey = new Map(); // key -> { value, expires_at }
/**
* Fetch current Bitcoin tip height and hash from mempool.space.
* Derive a stable cache key for a given source config. Keeps the cache per
* source so a caller who switches from mempool.space to an RPC between calls
* doesn't briefly get a stale value from the other.
*/
function sourceCacheKey(source) {
if (!source || source.type === 'mempool-space') return 'mempool-space';
if (source.type === 'bitcoind-rpc') {
return `bitcoind-rpc:${source.url || ''}`;
}
return `unknown:${source.type || 'none'}`;
}
/**
* Encode HTTP Basic auth for a user/password pair. Safe to call with either
* the explicit basic string (preferred: caller can pre-encode once) or the
* raw credentials.
*/
function basicAuthHeader(source) {
if (source.authBasic) return `Basic ${source.authBasic}`;
const user = source.user || '';
const password = source.password || '';
const b64 = Buffer.from(`${user}:${password}`).toString('base64');
return `Basic ${b64}`;
}
/**
* Fetch tip from a bitcoind JSON-RPC endpoint. Issues two RPC calls
* (`getblockcount` + `getbestblockhash`) and returns the same
* `{ height, hash, observed_at }` shape as the mempool.space path.
*
* Returns { height, hash, observed_at } where observed_at is an ISO-8601
* timestamp of when the fetch landed on this host (not a block timestamp).
* Cached for 60 seconds in-process. On fetch error, throws so callers can
* decide whether to ship the score without the anchor.
*
* @param {object} [opts]
* @param {Function} [opts.fetchImpl] Injected fetch for tests. Defaults to globalThis.fetch.
* @param {number} [opts.timeoutMs] Per-request timeout in ms. Default 3000.
* @returns {Promise<{height: number, hash: string, observed_at: string}>}
* Throws on network error, HTTP non-2xx, JSON-RPC-level error, or malformed
* fields. Callers are expected to catch and decide whether to degrade to the
* default source (`bitcoin-pulse.js` and the identity-score server both do
* this).
*/
async function getChaintip(opts = {}) {
const now = Date.now();
if (cached && cached.expires_at > now) {
return cached.value;
async function fetchTipFromRpc(source, fetchImpl, timeoutMs) {
if (!source.url) throw new Error('chaintip: source.url required for bitcoind-rpc');
async function rpc(method, params = []) {
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetchImpl(source.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: basicAuthHeader(source),
},
body: JSON.stringify({ jsonrpc: '1.0', id: 'chaintip', method, params }),
signal: controller.signal,
});
if (!res.ok) throw new Error(`chaintip: rpc ${method} returned HTTP ${res.status}`);
const body = await res.json();
if (body && body.error) {
throw new Error(`chaintip: rpc ${method} error: ${JSON.stringify(body.error)}`);
}
return body.result;
} finally {
clearTimeout(t);
}
}
const fetchImpl = opts.fetchImpl || globalThis.fetch;
if (typeof fetchImpl !== 'function') {
throw new Error('chaintip: no fetch implementation available (Node >= 18 or pass opts.fetchImpl)');
const [height, hash] = await Promise.all([
rpc('getblockcount'),
rpc('getbestblockhash'),
]);
if (!Number.isFinite(height) || height <= 0) {
throw new Error(`chaintip: rpc getblockcount returned invalid height: ${JSON.stringify(height)}`);
}
const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : 3000;
if (typeof hash !== 'string' || !/^[0-9a-f]{64}$/.test(hash)) {
throw new Error(`chaintip: rpc getbestblockhash returned invalid hash: ${JSON.stringify(hash)}`);
}
return {
height,
hash,
observed_at: new Date().toISOString(),
};
}
/**
* Fetch tip from mempool.space (the default source). Same shape as the RPC
* path. Preserved verbatim from the pre-RPC version so existing callers see
* identical behavior.
*/
async function fetchTipFromMempoolSpace(fetchImpl, timeoutMs) {
async function getText(url) {

@@ -89,3 +158,3 @@ const controller = new AbortController();

const value = {
return {
height,

@@ -95,3 +164,67 @@ hash: hashText,

};
cached = { value, expires_at: now + CACHE_TTL_MS };
}
/**
* Fetch current Bitcoin tip height and hash.
*
* Returns `{ height, hash, observed_at }` where observed_at is an ISO-8601
* timestamp of when the fetch landed on this host (not a block timestamp).
* Cached for 60 seconds in-process per source key.
*
* Default source: mempool.space HTTP endpoints. Same path as every prior
* release. Backwards-compatible: a caller who does not pass `opts.source`
* sees byte-identical behavior to the pre-0.7.0-beta.2 module.
*
* Opt-in RPC source:
* getChaintip({
* source: {
* type: 'bitcoind-rpc',
* url: 'http://lightning.lan:8332',
* user: 'zeke',
* password: process.env.BITCOIN_RPC_PASSWORD,
* // or pre-encoded: authBasic: 'base64...'
* },
* });
* When set, the module POSTs JSON-RPC `getblockcount` + `getbestblockhash`
* to the given endpoint instead of hitting mempool.space. The returned
* envelope schema is unchanged — callers cannot tell which source produced
* the value, which is the whole point: the signed score payload stays
* wire-compatible with all existing verifiers.
*
* On fetch error, throws. Callers decide whether to degrade (for example,
* the identity-score server swallows chaintip errors into
* `bitcoin_tip_error` rather than failing the score).
*
* @param {object} [opts]
* @param {Function} [opts.fetchImpl] Injected fetch for tests. Defaults to globalThis.fetch.
* @param {number} [opts.timeoutMs] Per-request timeout in ms. Default 3000.
* @param {object} [opts.source] { type: 'mempool-space' | 'bitcoind-rpc', ...fields }
* @returns {Promise<{height: number, hash: string, observed_at: string}>}
*/
async function getChaintip(opts = {}) {
const now = Date.now();
const source = opts.source || { type: 'mempool-space' };
const key = sourceCacheKey(source);
const existing = cacheByKey.get(key);
if (existing && existing.expires_at > now) {
return existing.value;
}
const fetchImpl = opts.fetchImpl || globalThis.fetch;
if (typeof fetchImpl !== 'function') {
throw new Error('chaintip: no fetch implementation available (Node >= 18 or pass opts.fetchImpl)');
}
const timeoutMs = typeof opts.timeoutMs === 'number' ? opts.timeoutMs : 3000;
let value;
if (source.type === 'bitcoind-rpc') {
value = await fetchTipFromRpc(source, fetchImpl, timeoutMs);
} else if (source.type === 'mempool-space' || !source.type) {
value = await fetchTipFromMempoolSpace(fetchImpl, timeoutMs);
} else {
throw new Error(`chaintip: unknown source.type ${JSON.stringify(source.type)}`);
}
cacheByKey.set(key, { value, expires_at: now + CACHE_TTL_MS });
return value;

@@ -105,3 +238,3 @@ }

function resetChaintipCache() {
cached = null;
cacheByKey.clear();
}

@@ -108,0 +241,0 @@