insumer-verify 1.0.1 → 1.1.1
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.
- package/README.md +18 -1
- package/build/index.d.ts +2 -0
- package/build/index.js +39 -5
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# insumer-verify
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Client-side verifier for [InsumerAPI](https://insumermodel.com/developers/) attestations. Validates ECDSA P-256 signatures, condition hashes, block freshness, and attestation expiry. Zero runtime dependencies. Web Crypto API. Node.js 18+ and modern browsers.
|
|
4
|
+
|
|
5
|
+
**In production:** [DJD Agent Score](https://github.com/jacobsd32-cpu/djdagentscore) (Coinbase x402 ecosystem) uses insumer-verify for client-side cryptographic verification in their AI agent wallet trust scoring pipeline. [Case study](https://insumermodel.com/blog/djd-agent-score-insumer-api-integration.html).
|
|
6
|
+
|
|
7
|
+
Part of the InsumerAPI ecosystem: [REST API](https://insumermodel.com/developers/) (17 endpoints, 31 chains) | [MCP server](https://www.npmjs.com/package/mcp-server-insumer) (npm) | [LangChain](https://pypi.org/project/langchain-insumer/) (PyPI) | [OpenAI GPT](https://chatgpt.com/g/g-699c5e43ce2481918b3f1e7f144c8a49-insumerapi-verify) (GPT Store)
|
|
4
8
|
|
|
5
9
|
## Install
|
|
6
10
|
|
|
@@ -91,6 +95,18 @@ const result = await verifyAttestation(apiResponse, { maxAge: 120 });
|
|
|
91
95
|
|
|
92
96
|
Results without `blockTimestamp` (Covalent and Solana chains) are skipped, not treated as failures.
|
|
93
97
|
|
|
98
|
+
### JWKS key discovery
|
|
99
|
+
|
|
100
|
+
By default, insumer-verify uses the hardcoded InsumerAPI public key. You can opt in to dynamic key discovery via the JWKS endpoint:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const result = await verifyAttestation(apiResponse, {
|
|
104
|
+
jwksUrl: "https://insumermodel.com/.well-known/jwks.json"
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
When `jwksUrl` is set, the library fetches the JWKS, matches the key by `kid` from the attestation response, and uses it for signature verification. This enables automatic key rotation without library updates.
|
|
109
|
+
|
|
94
110
|
## API
|
|
95
111
|
|
|
96
112
|
### `verifyAttestation(response, options?)`
|
|
@@ -99,6 +115,7 @@ Results without `blockTimestamp` (Covalent and Solana chains) are skipped, not t
|
|
|
99
115
|
|-----------|------|-------------|
|
|
100
116
|
| `response` | `unknown` | Full InsumerAPI response (must contain `data.attestation` and `data.sig`) |
|
|
101
117
|
| `options.maxAge` | `number` | Optional max age in seconds for block freshness check |
|
|
118
|
+
| `options.jwksUrl` | `string` | Optional JWKS URL for dynamic key discovery (e.g. `https://insumermodel.com/.well-known/jwks.json`) |
|
|
102
119
|
|
|
103
120
|
Returns `Promise<VerifyResult>`:
|
|
104
121
|
|
package/build/index.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface CheckResult {
|
|
|
23
23
|
export interface VerifyOptions {
|
|
24
24
|
/** Maximum acceptable age of blockTimestamp in seconds. Results without blockTimestamp are skipped. */
|
|
25
25
|
maxAge?: number;
|
|
26
|
+
/** JWKS URL for dynamic key discovery. When set, fetches the signing key from this URL instead of using the hardcoded key. Example: "https://insumermodel.com/.well-known/jwks.json" */
|
|
27
|
+
jwksUrl?: string;
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
30
|
* Verify an InsumerAPI attestation response.
|
package/build/index.js
CHANGED
|
@@ -34,6 +34,21 @@ function bytesToHex(bytes) {
|
|
|
34
34
|
}
|
|
35
35
|
return hex;
|
|
36
36
|
}
|
|
37
|
+
async function fetchJwksKey(jwksUrl, kid) {
|
|
38
|
+
const res = await fetch(jwksUrl);
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`JWKS fetch failed: ${res.status} ${res.statusText}`);
|
|
41
|
+
}
|
|
42
|
+
const jwks = (await res.json());
|
|
43
|
+
if (!jwks.keys || !Array.isArray(jwks.keys) || jwks.keys.length === 0) {
|
|
44
|
+
throw new Error("JWKS response contains no keys");
|
|
45
|
+
}
|
|
46
|
+
// Match by kid if provided, otherwise use first key
|
|
47
|
+
const key = kid
|
|
48
|
+
? jwks.keys.find((k) => k.kid === kid) || jwks.keys[0]
|
|
49
|
+
: jwks.keys[0];
|
|
50
|
+
return { kty: key.kty, crv: key.crv, x: key.x, y: key.y };
|
|
51
|
+
}
|
|
37
52
|
function parseResponse(response) {
|
|
38
53
|
const obj = response;
|
|
39
54
|
const data = obj?.data;
|
|
@@ -63,15 +78,16 @@ function parseResponse(response) {
|
|
|
63
78
|
if (typeof attestation.expiresAt !== "string") {
|
|
64
79
|
throw new Error("Invalid response: missing attestation.expiresAt");
|
|
65
80
|
}
|
|
66
|
-
|
|
81
|
+
const kid = data.kid;
|
|
82
|
+
return { data: { attestation, sig, kid } };
|
|
67
83
|
}
|
|
68
84
|
// ── Verification checks ───────────────────────────────────────────
|
|
69
|
-
async function checkSignature(attestation, sig) {
|
|
85
|
+
async function checkSignature(attestation, sig, keyJwk) {
|
|
70
86
|
if (!subtle) {
|
|
71
87
|
return { passed: false, reason: "Web Crypto API not available" };
|
|
72
88
|
}
|
|
73
89
|
try {
|
|
74
|
-
const key = await subtle.importKey("jwk", PUBLIC_KEY_JWK, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
90
|
+
const key = await subtle.importKey("jwk", keyJwk || PUBLIC_KEY_JWK, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
|
|
75
91
|
// Reconstruct the exact payload that was signed server-side.
|
|
76
92
|
// The Cloud Function signs: JSON.stringify({ id, pass, results, attestedAt })
|
|
77
93
|
const payload = JSON.stringify({
|
|
@@ -167,9 +183,27 @@ function checkExpiry(attestation) {
|
|
|
167
183
|
*/
|
|
168
184
|
export async function verifyAttestation(response, options) {
|
|
169
185
|
const parsed = parseResponse(response);
|
|
170
|
-
const { attestation, sig } = parsed.data;
|
|
186
|
+
const { attestation, sig, kid } = parsed.data;
|
|
187
|
+
// If jwksUrl is provided, fetch the signing key dynamically
|
|
188
|
+
let keyJwk;
|
|
189
|
+
if (options?.jwksUrl) {
|
|
190
|
+
try {
|
|
191
|
+
keyJwk = await fetchJwksKey(options.jwksUrl, kid);
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
return {
|
|
195
|
+
valid: false,
|
|
196
|
+
checks: {
|
|
197
|
+
signature: { passed: false, reason: `JWKS fetch error: ${e.message}` },
|
|
198
|
+
conditionHashes: { passed: false, reason: "Skipped (JWKS fetch failed)" },
|
|
199
|
+
freshness: { passed: false, reason: "Skipped (JWKS fetch failed)" },
|
|
200
|
+
expiry: { passed: false, reason: "Skipped (JWKS fetch failed)" },
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
171
205
|
const [signature, conditionHashes, freshness, expiry] = await Promise.all([
|
|
172
|
-
checkSignature(attestation, sig),
|
|
206
|
+
checkSignature(attestation, sig, keyJwk),
|
|
173
207
|
checkConditionHashes(attestation.results),
|
|
174
208
|
Promise.resolve(checkFreshness(attestation.results, options?.maxAge)),
|
|
175
209
|
Promise.resolve(checkExpiry(attestation)),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insumer-verify",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Client-side verifier for InsumerAPI attestations. ECDSA P-256 signatures, condition hashes, block freshness, expiry. Zero dependencies. Used by DJD Agent Score (Coinbase x402).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"types": "build/index.d.ts",
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
"verification",
|
|
27
27
|
"blockchain",
|
|
28
28
|
"on-chain",
|
|
29
|
-
"web-crypto"
|
|
29
|
+
"web-crypto",
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"token-gating",
|
|
32
|
+
"merkle-proof",
|
|
33
|
+
"agent-trust"
|
|
30
34
|
],
|
|
31
35
|
"author": "Douglas Borthwick",
|
|
32
36
|
"license": "MIT",
|