@yantrixai/openproof-verify 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +114 -0
  2. package/index.d.ts +56 -0
  3. package/index.js +173 -0
  4. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # openproof-verify
2
+
3
+ Verify [OpenProof](https://openproof.io) cryptographic signatures on API responses.
4
+
5
+ OpenProof is an open protocol that adds ECDSA signatures to every API response — so AI agents can cryptographically verify that data came from the claimed source and hasn't been tampered with.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install openproof-verify
11
+ # or
12
+ npm install openproof-verify ethers # recommended for full signature recovery
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```javascript
18
+ const { verify, verifyResponse } = require('openproof-verify')
19
+
20
+ // From a Yantrix API response:
21
+ const response = await fetch('https://agent-registry.yantrix.ai/v1/agents/agt_abc123')
22
+ const json = await response.json()
23
+
24
+ // Verify the signature
25
+ const result = verifyResponse(json)
26
+
27
+ if (result.valid) {
28
+ console.log('✓ Response verified')
29
+ console.log(' Signed by:', result.signer)
30
+ console.log(' Signed at:', new Date(result.signed_at * 1000).toISOString())
31
+ } else {
32
+ console.error('✗ Verification failed:', result.reason)
33
+ }
34
+ ```
35
+
36
+ ### With trusted signers
37
+
38
+ ```javascript
39
+ const result = verify(json.data, json._proof, {
40
+ trustedSigners: ['0x41a024c1c89fd30122c8b184de99cbe751eac970'],
41
+ maxAgeSeconds: 60, // reject signatures older than 1 minute
42
+ })
43
+ ```
44
+
45
+ ### Remote verification
46
+
47
+ ```javascript
48
+ const { verifyRemote } = require('openproof-verify')
49
+
50
+ const result = await verifyRemote(
51
+ 'https://agent-registry.yantrix.ai',
52
+ json.data,
53
+ json._proof
54
+ )
55
+ ```
56
+
57
+ ## What is OpenProof?
58
+
59
+ OpenProof adds a `_proof` envelope to every API response:
60
+
61
+ ```json
62
+ {
63
+ "data": { "price": 2081.41, "symbol": "ETH" },
64
+ "_proof": {
65
+ "call_id": "ytx_abc123def456",
66
+ "endpoint": "/v1/price/ETH",
67
+ "payload_hash": "sha256:9f86d081...",
68
+ "timestamp": 1711234567,
69
+ "signer": "0x41A024c1C89Fd30122c8b184de99cbE751eaC970",
70
+ "signature": "0x3ad7f1..."
71
+ }
72
+ }
73
+ ```
74
+
75
+ The signature proves:
76
+ 1. The response came from the owner of `0x41A024...`
77
+ 2. The `data` field hasn't been modified since signing
78
+ 3. The response was signed at `timestamp`
79
+
80
+ ## API
81
+
82
+ ### `verify(data, trust, options?)`
83
+ Verify locally. Returns `{ valid, signer, signed_at, reason }`.
84
+
85
+ ### `verifyResponse(response, options?)`
86
+ Verify a full response object with `data` and `_proof` fields.
87
+
88
+ ### `verifyRemote(apiBaseUrl, data, trust)`
89
+ Verify via the API's own `/v1/verify/signature` endpoint. Returns a Promise.
90
+
91
+ ### `canonical(obj)`
92
+ Serialize an object to canonical JSON (sorted keys, no whitespace).
93
+
94
+ ## Options
95
+
96
+ | Option | Type | Default | Description |
97
+ |--------|------|---------|-------------|
98
+ | `trustedSigners` | `string[]` | `undefined` | Allowlist of trusted signer addresses |
99
+ | `maxAgeSeconds` | `number` | `300` | Reject signatures older than this. Set to `0` to disable. |
100
+
101
+ ## OpenProof-compliant APIs
102
+
103
+ - [Yantrix Agent Registry](https://agent-registry.yantrix.ai)
104
+ - [Yantrix Agent Session](https://agent-session.yantrix.ai)
105
+ - [Yantrix Relay](https://relay.yantrix.ai)
106
+ - [Full list at openproof.io/registry](https://openproof.io/registry)
107
+
108
+ ## Protocol Spec
109
+
110
+ [SPEC.md](https://github.com/yantrix-ai/openproof/blob/main/SPEC.md)
111
+
112
+ ## License
113
+
114
+ MIT
package/index.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ export interface TrustEnvelope {
2
+ call_id: string
3
+ endpoint: string
4
+ payload_hash: string
5
+ timestamp: number
6
+ signer: string
7
+ signature: string
8
+ receipt_url?: string
9
+ }
10
+
11
+ export interface VerifyResult {
12
+ valid: boolean
13
+ signer?: string
14
+ signed_at?: number
15
+ call_id?: string
16
+ endpoint?: string
17
+ reason?: string
18
+ }
19
+
20
+ export interface VerifyOptions {
21
+ /** List of trusted signer addresses (lowercase). If provided, signer must be in this list. */
22
+ trustedSigners?: string[]
23
+ /** Maximum age of signature in seconds. 0 = skip age check. Default: 300 (5 minutes) */
24
+ maxAgeSeconds?: number
25
+ }
26
+
27
+ /**
28
+ * Verify a OpenProof signature locally.
29
+ */
30
+ export function verify(
31
+ data: object,
32
+ trust: TrustEnvelope,
33
+ options?: VerifyOptions
34
+ ): VerifyResult
35
+
36
+ /**
37
+ * Verify a OpenProof signature remotely via the API's /v1/verify/signature endpoint.
38
+ */
39
+ export function verifyRemote(
40
+ apiBaseUrl: string,
41
+ data: object,
42
+ trust: TrustEnvelope
43
+ ): Promise<VerifyResult>
44
+
45
+ /**
46
+ * Extract and verify OpenProof from a full API response containing `data` and `_proof`.
47
+ */
48
+ export function verifyResponse(
49
+ response: { data: object; _proof: TrustEnvelope; [key: string]: any },
50
+ options?: VerifyOptions
51
+ ): VerifyResult
52
+
53
+ /**
54
+ * Canonicalize an object to deterministic JSON (sorted keys, no whitespace).
55
+ */
56
+ export function canonical(obj: object): string
package/index.js ADDED
@@ -0,0 +1,173 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * openproof-verify
5
+ * Verify OpenProof signatures on API responses.
6
+ * https://openproof.io
7
+ */
8
+
9
+ const crypto = require('crypto')
10
+
11
+ /**
12
+ * Canonicalize an object to deterministic JSON.
13
+ * Sorted keys, no whitespace.
14
+ */
15
+ function canonical(obj) {
16
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
17
+ return JSON.stringify(obj)
18
+ }
19
+ const sorted = Object.keys(obj).sort().reduce((acc, key) => {
20
+ acc[key] = obj[key]
21
+ return acc
22
+ }, {})
23
+ return JSON.stringify(sorted, (_, v) => {
24
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
25
+ return Object.keys(v).sort().reduce((acc, k) => {
26
+ acc[k] = v[k]
27
+ return acc
28
+ }, {})
29
+ }
30
+ return v
31
+ })
32
+ }
33
+
34
+ /**
35
+ * Recover Ethereum address from a personal_sign signature.
36
+ * Works in Node.js without ethers.js dependency.
37
+ */
38
+ function recoverAddress(message, signature) {
39
+ try {
40
+ // Try using ethers if available
41
+ const ethers = require('ethers')
42
+ return ethers.verifyMessage(message, signature).toLowerCase()
43
+ } catch (e) {
44
+ // Fallback: manual recovery using secp256k1
45
+ try {
46
+ const { ecdsaRecover, publicKeyConvert } = require('secp256k1')
47
+ const prefix = `\x19Ethereum Signed Message:\n${Buffer.byteLength(message)}`
48
+ const msgHash = crypto.createHash('sha256')
49
+ .update(Buffer.from(prefix + message))
50
+ .digest()
51
+ const sigBuf = Buffer.from(signature.replace('0x', ''), 'hex')
52
+ const r = sigBuf.slice(0, 32)
53
+ const s = sigBuf.slice(32, 64)
54
+ let v = sigBuf[64]
55
+ if (v >= 27) v -= 27
56
+ const pubKey = ecdsaRecover(Buffer.concat([r, s]), v, msgHash, false)
57
+ const pubKeyHash = crypto.createHash('sha256').update(pubKeyConvert(pubKey, false).slice(1)).digest()
58
+ return '0x' + pubKeyHash.slice(-20).toString('hex').toLowerCase()
59
+ } catch (e2) {
60
+ throw new Error(`Cannot recover address: no ethers or secp256k1 available. Install ethers: npm install ethers`)
61
+ }
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Verify a OpenProof signature.
67
+ *
68
+ * @param {object} data - The `data` field from the API response
69
+ * @param {object} trust - The `_proof` envelope from the API response
70
+ * @param {object} [options] - Verification options
71
+ * @param {string[]} [options.trustedSigners] - List of trusted signer addresses (lowercase)
72
+ * @param {number} [options.maxAgeSeconds=300] - Max age of signature in seconds (0 = skip)
73
+ * @returns {{ valid: boolean, signer?: string, signed_at?: number, reason?: string }}
74
+ */
75
+ function verify(data, trust, options = {}) {
76
+ const { trustedSigners, maxAgeSeconds = 300 } = options
77
+
78
+ if (!trust || typeof trust !== 'object') {
79
+ return { valid: false, reason: 'Missing _proof envelope' }
80
+ }
81
+
82
+ const { call_id, endpoint, payload_hash, timestamp, signer, signature } = trust
83
+
84
+ if (!call_id || !endpoint || !payload_hash || !timestamp || !signer || !signature) {
85
+ return { valid: false, reason: 'Incomplete _proof envelope — missing required fields' }
86
+ }
87
+
88
+ // Step 1: Verify payload hash
89
+ const canonicalData = canonical(data)
90
+ const computedHash = crypto.createHash('sha256').update(canonicalData).digest('hex')
91
+ const expectedHash = payload_hash.replace('sha256:', '')
92
+
93
+ if (computedHash !== expectedHash) {
94
+ return { valid: false, reason: 'Payload hash mismatch — data may have been tampered with' }
95
+ }
96
+
97
+ // Step 2: Check timestamp freshness
98
+ if (maxAgeSeconds > 0) {
99
+ const now = Math.floor(Date.now() / 1000)
100
+ const age = now - timestamp
101
+ if (age > maxAgeSeconds) {
102
+ return { valid: false, reason: `Signature expired — ${age}s old, max ${maxAgeSeconds}s` }
103
+ }
104
+ }
105
+
106
+ // Step 3: Recover signer from signature
107
+ const message = `${call_id}:${endpoint}:sha256:${computedHash}:${timestamp}`
108
+ let recovered
109
+ try {
110
+ recovered = recoverAddress(message, signature)
111
+ } catch (e) {
112
+ return { valid: false, reason: `Signature recovery failed: ${e.message}` }
113
+ }
114
+
115
+ if (recovered !== signer.toLowerCase()) {
116
+ return { valid: false, reason: `Signer mismatch — expected ${signer}, got ${recovered}` }
117
+ }
118
+
119
+ // Step 4: Check trusted signers (optional)
120
+ if (trustedSigners && trustedSigners.length > 0) {
121
+ const normalizedTrusted = trustedSigners.map(s => s.toLowerCase())
122
+ if (!normalizedTrusted.includes(recovered)) {
123
+ return { valid: false, reason: `Signer ${recovered} not in trusted signers list` }
124
+ }
125
+ }
126
+
127
+ return {
128
+ valid: true,
129
+ signer: recovered,
130
+ signed_at: timestamp,
131
+ call_id,
132
+ endpoint,
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Verify a OpenProof signature asynchronously (remote verification).
138
+ * Calls the API's /v1/verify/signature endpoint.
139
+ *
140
+ * @param {string} apiBaseUrl - Base URL of the API (e.g. https://agent-registry.yantrix.ai)
141
+ * @param {object} data - The `data` field from the response
142
+ * @param {object} trust - The `_proof` envelope from the response
143
+ * @returns {Promise<{ valid: boolean, signer?: string, signed_at?: number }>}
144
+ */
145
+ async function verifyRemote(apiBaseUrl, data, trust) {
146
+ const url = `${apiBaseUrl.replace(/\/$/, '')}/v1/verify/signature`
147
+ const response = await fetch(url, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json' },
150
+ body: JSON.stringify({ envelope: trust, payload: data }),
151
+ })
152
+ const result = await response.json()
153
+ return result.data || result
154
+ }
155
+
156
+ /**
157
+ * Extract and verify OpenProof from a full API response object.
158
+ *
159
+ * @param {object} response - Full API response containing `data` and `_proof`
160
+ * @param {object} [options] - Same options as verify()
161
+ */
162
+ function verifyResponse(response, options = {}) {
163
+ if (!response || typeof response !== 'object') {
164
+ return { valid: false, reason: 'Invalid response object' }
165
+ }
166
+ const { data, _proof } = response
167
+ if (!_proof) {
168
+ return { valid: false, reason: 'Response does not include _proof envelope — not OpenProof compliant' }
169
+ }
170
+ return verify(data, _proof, options)
171
+ }
172
+
173
+ module.exports = { verify, verifyRemote, verifyResponse, canonical }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@yantrixai/openproof-verify",
3
+ "version": "1.0.0",
4
+ "description": "Verify OpenProof cryptographic signatures on API responses. For AI agents that need to trust external data.",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "scripts": {
8
+ "test": "node test.js"
9
+ },
10
+ "keywords": [
11
+ "openproof",
12
+ "api",
13
+ "signature",
14
+ "verification",
15
+ "ecdsa",
16
+ "ethereum",
17
+ "ai-agents",
18
+ "x402",
19
+ "yantrix"
20
+ ],
21
+ "author": "Yantrix <hello@yantrix.ai>",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/yantrix-ai/openproof"
26
+ },
27
+ "homepage": "https://openproof.io",
28
+ "peerDependencies": {
29
+ "ethers": ">=5.0.0"
30
+ },
31
+ "peerDependenciesMeta": {
32
+ "ethers": {
33
+ "optional": true
34
+ }
35
+ },
36
+ "engines": {
37
+ "node": ">=18.0.0"
38
+ }
39
+ }