@powforge/identity
Advanced tools
+24
-0
| # Changelog | ||
| ## 0.7.1 | ||
| ### Added | ||
| - `checkFreshness(scoreResponse, currentTipHeight)` — pure-function helper | ||
| that validates a signed DoI score's chaintip-bound freshness window | ||
| without round-tripping to the oracle. Returns `{fresh, reason, | ||
| signed_tip_height, current_tip_height, blocks_elapsed, freshness_blocks, | ||
| fresh_until_height}`. Does NOT verify the schnorr signature; pair with | ||
| the existing schnorr verify path on the caller side. | ||
| - The oracle now signs `freshness_blocks` alongside `bitcoin_tip` inside | ||
| the schnorr-covered envelope. Default 6 blocks (~60 min). Verifiers can | ||
| reject a stale score even when the signature still verifies. The oracle | ||
| also exposes a public `/verify-freshness` endpoint that does the same | ||
| check server-side for callers without the SDK. | ||
| ### Compat | ||
| - Additive only. SDK callers that never reach for `checkFreshness` see | ||
| byte-identical behavior. Oracle responses keep their previous shape; | ||
| `freshness_blocks` is a new field, not a renamed one. Pre-existing | ||
| schnorr verifiers that canonicalize the full payload before checking | ||
| the signature pick the field up automatically. | ||
| ## 0.7.0 | ||
@@ -4,0 +28,0 @@ |
+2
-2
| { | ||
| "name": "@powforge/identity", | ||
| "version": "0.7.0", | ||
| "version": "0.7.1", | ||
| "description": "Depth-of-Identity SDK for Nostr. Measures accumulated irreversible work across four dimensions of irreversible work (social, access, vouch, economic). Try it live at powforge.dev/explorer.", | ||
@@ -18,3 +18,3 @@ "keywords": [ | ||
| "scripts": { | ||
| "test": "node --test tests/smoke.test.js tests/publish.test.js tests/temporal.test.js tests/zap-bolt11-dedup.test.js tests/zap-receipt-verify.test.js tests/vouch-cycle.test.js tests/indirect-vouch-cycle.test.js tests/chaintip.test.js tests/spatial-deprecation.test.js" | ||
| "test": "node --test tests/smoke.test.js tests/publish.test.js tests/temporal.test.js tests/zap-bolt11-dedup.test.js tests/zap-receipt-verify.test.js tests/vouch-cycle.test.js tests/indirect-vouch-cycle.test.js tests/chaintip.test.js tests/spatial-deprecation.test.js tests/freshness.test.js" | ||
| }, | ||
@@ -21,0 +21,0 @@ "license": "MIT", |
+89
-1
@@ -792,2 +792,90 @@ /** | ||
| module.exports = { getIdentityDepth, queryRelay, scoreSpatial, scoreSocial, scoreAccess, scoreVouch, scoreEconomic, verifyEventFull, withinTemporalBounds, verifyZapReceipt, detectDirectCycle, detectIndirectCycle, getChaintip, signWithChaintip, resetChaintipCache }; | ||
| /** | ||
| * checkFreshness — pure-function freshness validator for a signed score. | ||
| * | ||
| * Validates the chaintip-binding window on a signed DoI score WITHOUT | ||
| * round-tripping to the oracle. Assumes the schnorr signature has already | ||
| * been verified (or will be — this function does NOT verify the signature, | ||
| * only the freshness window). | ||
| * | ||
| * Inputs: | ||
| * scoreResponse the signed envelope returned by the oracle. Must carry | ||
| * `bitcoin_tip.height` (number) and `freshness_blocks` | ||
| * (number). Older oracle builds that pre-date the | ||
| * freshness cert (no `freshness_blocks` field) cannot | ||
| * be checked — this function returns fresh:false with | ||
| * reason 'no_freshness_window' so callers don't silently | ||
| * accept stale stale scores from older oracles. | ||
| * currentTipHeight the caller's own view of the current bitcoin chaintip | ||
| * height. Caller fetches this however they like | ||
| * (mempool.space, local bitcoind, getChaintip()). | ||
| * | ||
| * Returns: | ||
| * { | ||
| * fresh: bool, | ||
| * reason: string|null, | ||
| * signed_tip_height: number|null, | ||
| * current_tip_height: number, | ||
| * blocks_elapsed: number|null, | ||
| * freshness_blocks: number|null, | ||
| * fresh_until_height: number|null, | ||
| * } | ||
| * | ||
| * reason values: | ||
| * 'no_bitcoin_tip' — score has no bitcoin_tip binding (legacy) | ||
| * 'no_freshness_window' — score has tip but no freshness_blocks field | ||
| * 'future_tip' — signed_tip_height > current_tip_height | ||
| * (signer claims a height the caller cannot see; | ||
| * could be reorg, lying signer, or stale caller) | ||
| * 'stale' — gap exceeds the freshness window | ||
| * null — fresh | ||
| * | ||
| * Edge cases: | ||
| * blocks_elapsed === freshness_blocks → fresh (boundary inclusive). | ||
| * blocks_elapsed === 0 → fresh. | ||
| * signed_tip > current → fresh:false, reason:'future_tip'. | ||
| */ | ||
| function checkFreshness(scoreResponse, currentTipHeight) { | ||
| if (typeof currentTipHeight !== 'number' || !Number.isFinite(currentTipHeight) || currentTipHeight < 0) { | ||
| throw new Error('checkFreshness: currentTipHeight must be a non-negative finite number'); | ||
| } | ||
| const result = { | ||
| fresh: false, | ||
| reason: null, | ||
| signed_tip_height: null, | ||
| current_tip_height: currentTipHeight, | ||
| blocks_elapsed: null, | ||
| freshness_blocks: null, | ||
| fresh_until_height: null, | ||
| }; | ||
| if (!scoreResponse || typeof scoreResponse !== 'object') { | ||
| result.reason = 'no_bitcoin_tip'; | ||
| return result; | ||
| } | ||
| const tip = scoreResponse.bitcoin_tip; | ||
| if (!tip || typeof tip.height !== 'number') { | ||
| result.reason = 'no_bitcoin_tip'; | ||
| return result; | ||
| } | ||
| result.signed_tip_height = tip.height; | ||
| const fb = scoreResponse.freshness_blocks; | ||
| if (typeof fb !== 'number' || !Number.isFinite(fb) || fb < 1) { | ||
| result.reason = 'no_freshness_window'; | ||
| return result; | ||
| } | ||
| result.freshness_blocks = fb; | ||
| result.fresh_until_height = tip.height + fb; | ||
| result.blocks_elapsed = currentTipHeight - tip.height; | ||
| if (result.blocks_elapsed < 0) { | ||
| result.reason = 'future_tip'; | ||
| return result; | ||
| } | ||
| if (result.blocks_elapsed > fb) { | ||
| result.reason = 'stale'; | ||
| return result; | ||
| } | ||
| result.fresh = true; | ||
| return result; | ||
| } | ||
| module.exports = { getIdentityDepth, queryRelay, scoreSpatial, scoreSocial, scoreAccess, scoreVouch, scoreEconomic, verifyEventFull, withinTemporalBounds, verifyZapReceipt, detectDirectCycle, detectIndirectCycle, getChaintip, signWithChaintip, resetChaintipCache, checkFreshness }; |
70847
6.9%1174
8.1%