insumer-verify 1.1.4 → 1.2.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 CHANGED
@@ -4,7 +4,7 @@ Client-side verifier for [InsumerAPI](https://insumermodel.com/developers/) atte
4
4
 
5
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
6
 
7
- Part of the InsumerAPI ecosystem: [REST API](https://insumermodel.com/developers/) (25 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)
7
+ Part of the InsumerAPI ecosystem: [REST API](https://insumermodel.com/developers/) (25 endpoints, 32 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)
8
8
 
9
9
  ## Install
10
10
 
@@ -99,7 +99,7 @@ The attestation response you're verifying looks like this:
99
99
  "sig": "MEUCIQD...(base64 ECDSA signature)...",
100
100
  "kid": "insumer-attest-v1"
101
101
  },
102
- "meta": { "version": "1.0", "creditsCharged": 1, "creditsRemaining": 99 }
102
+ "meta": { "version": "1.0", "timestamp": "2026-02-28T12:34:57.000Z", "creditsCharged": 1, "creditsRemaining": 99 }
103
103
  }
104
104
  ```
105
105
 
package/build/index.d.ts CHANGED
@@ -4,6 +4,10 @@
4
4
  * Validates ECDSA P-256 signatures, condition hashes, block freshness,
5
5
  * and attestation expiry using the Web Crypto API. Zero dependencies.
6
6
  * Works in Node.js 18+ and modern browsers.
7
+ *
8
+ * Accepts two input formats (auto-detected):
9
+ * - **JWT string**: ES256-signed JWT from POST /v1/attest with format: "jwt"
10
+ * - **Object**: Raw API response object with data.attestation and data.sig
7
11
  */
8
12
  export interface VerifyResult {
9
13
  valid: boolean;
@@ -29,14 +33,18 @@ export interface VerifyOptions {
29
33
  /**
30
34
  * Verify an InsumerAPI attestation response.
31
35
  *
32
- * Runs 4 independent checks:
33
- * 1. **Signature** ECDSA P-256 over {id, pass, results, attestedAt}
36
+ * Auto-detects input format:
37
+ * - **String** JWT verification path (ES256 signature via JWKS)
38
+ * - **Object** → Raw attestation response path (ECDSA P1363 signature)
39
+ *
40
+ * Both formats run the same 4 checks:
41
+ * 1. **Signature** — ECDSA P-256 verification
34
42
  * 2. **Condition hashes** — SHA-256 of canonical sorted-key JSON
35
43
  * 3. **Freshness** — blockTimestamp age vs caller-defined maxAge (optional)
36
- * 4. **Expiry** — whether the 30-minute attestation window has elapsed
44
+ * 4. **Expiry** — whether the attestation window has elapsed
37
45
  *
38
- * @param response The full API response object (must contain data.attestation and data.sig)
39
- * @param options Optional configuration: maxAge (seconds) for freshness check
46
+ * @param response JWT string or full API response object
47
+ * @param options Optional configuration: maxAge (seconds), jwksUrl
40
48
  * @returns Structured result with overall validity and per-check details
41
49
  */
42
50
  export declare function verifyAttestation(response: unknown, options?: VerifyOptions): Promise<VerifyResult>;
package/build/index.js CHANGED
@@ -4,6 +4,10 @@
4
4
  * Validates ECDSA P-256 signatures, condition hashes, block freshness,
5
5
  * and attestation expiry using the Web Crypto API. Zero dependencies.
6
6
  * Works in Node.js 18+ and modern browsers.
7
+ *
8
+ * Accepts two input formats (auto-detected):
9
+ * - **JWT string**: ES256-signed JWT from POST /v1/attest with format: "jwt"
10
+ * - **Object**: Raw API response object with data.attestation and data.sig
7
11
  */
8
12
  // ── Public key ─────────────────────────────────────────────────────
9
13
  /**
@@ -17,6 +21,7 @@ const PUBLIC_KEY_JWK = {
17
21
  y: "kn34HaxVSJfn8NxwNEBjjLkcrM_GDw1lgnqyADGuc4c",
18
22
  crv: "P-256",
19
23
  };
24
+ const DEFAULT_JWKS_URL = "https://insumermodel.com/.well-known/jwks.json";
20
25
  // ── Helpers ────────────────────────────────────────────────────────
21
26
  const subtle = globalThis.crypto?.subtle;
22
27
  function base64ToBytes(b64) {
@@ -27,6 +32,21 @@ function base64ToBytes(b64) {
27
32
  }
28
33
  return bytes;
29
34
  }
35
+ function base64UrlToBytes(b64url) {
36
+ // Convert base64url to standard base64
37
+ let b64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
38
+ // Add padding
39
+ const pad = b64.length % 4;
40
+ if (pad === 2)
41
+ b64 += "==";
42
+ else if (pad === 3)
43
+ b64 += "=";
44
+ return base64ToBytes(b64);
45
+ }
46
+ function base64UrlDecode(b64url) {
47
+ const bytes = base64UrlToBytes(b64url);
48
+ return new TextDecoder().decode(bytes);
49
+ }
30
50
  function bytesToHex(bytes) {
31
51
  let hex = "";
32
52
  for (let i = 0; i < bytes.length; i++) {
@@ -81,6 +101,63 @@ function parseResponse(response) {
81
101
  const kid = data.kid;
82
102
  return { data: { attestation, sig, kid } };
83
103
  }
104
+ function parseJwt(token) {
105
+ const parts = token.split(".");
106
+ if (parts.length !== 3) {
107
+ throw new Error("Invalid JWT: expected 3 dot-separated segments");
108
+ }
109
+ const [headerB64, payloadB64, sigB64] = parts;
110
+ let header;
111
+ let payload;
112
+ try {
113
+ header = JSON.parse(base64UrlDecode(headerB64));
114
+ }
115
+ catch {
116
+ throw new Error("Invalid JWT: malformed header");
117
+ }
118
+ try {
119
+ payload = JSON.parse(base64UrlDecode(payloadB64));
120
+ }
121
+ catch {
122
+ throw new Error("Invalid JWT: malformed payload");
123
+ }
124
+ const signatureBytes = base64UrlToBytes(sigB64);
125
+ return { header, payload, headerB64, payloadB64, signatureBytes };
126
+ }
127
+ /**
128
+ * Convert a DER-encoded ECDSA signature to raw IEEE P1363 format (r || s).
129
+ * jose/Node.js crypto may produce DER signatures; Web Crypto expects P1363.
130
+ */
131
+ function derToP1363(der, keySize = 32) {
132
+ if (der[0] !== 0x30)
133
+ return der; // Not DER, assume already P1363
134
+ let offset = 2;
135
+ // Parse r
136
+ if (der[offset] !== 0x02)
137
+ return der;
138
+ offset++;
139
+ const rLen = der[offset];
140
+ offset++;
141
+ let r = der.slice(offset, offset + rLen);
142
+ offset += rLen;
143
+ // Parse s
144
+ if (der[offset] !== 0x02)
145
+ return der;
146
+ offset++;
147
+ const sLen = der[offset];
148
+ offset++;
149
+ let s = der.slice(offset, offset + sLen);
150
+ // Remove leading zero padding
151
+ if (r.length > keySize)
152
+ r = r.slice(r.length - keySize);
153
+ if (s.length > keySize)
154
+ s = s.slice(s.length - keySize);
155
+ // Pad to fixed keySize
156
+ const result = new Uint8Array(keySize * 2);
157
+ result.set(r, keySize - r.length);
158
+ result.set(s, keySize * 2 - s.length);
159
+ return result;
160
+ }
84
161
  // ── Verification checks ───────────────────────────────────────────
85
162
  async function checkSignature(attestation, sig, keyJwk) {
86
163
  if (!subtle) {
@@ -110,6 +187,35 @@ async function checkSignature(attestation, sig, keyJwk) {
110
187
  };
111
188
  }
112
189
  }
190
+ async function checkJwtSignature(jwt, keyJwk) {
191
+ if (!subtle) {
192
+ return { passed: false, reason: "Web Crypto API not available" };
193
+ }
194
+ try {
195
+ if (jwt.header.alg !== "ES256") {
196
+ return { passed: false, reason: `Unsupported JWT algorithm: ${jwt.header.alg}` };
197
+ }
198
+ const key = await subtle.importKey("jwk", keyJwk || PUBLIC_KEY_JWK, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
199
+ // JWT signature is over "header.payload" (the raw base64url segments)
200
+ const signingInput = new TextEncoder().encode(`${jwt.headerB64}.${jwt.payloadB64}`);
201
+ // JWT ES256 signatures should be raw r||s (P1363, 64 bytes)
202
+ // but some libraries produce DER encoding — handle both
203
+ let sigBytes = jwt.signatureBytes;
204
+ if (sigBytes.length !== 64) {
205
+ sigBytes = derToP1363(sigBytes);
206
+ }
207
+ const valid = await subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, sigBytes.buffer, signingInput);
208
+ return valid
209
+ ? { passed: true }
210
+ : { passed: false, reason: "JWT signature does not match payload" };
211
+ }
212
+ catch (e) {
213
+ return {
214
+ passed: false,
215
+ reason: `JWT signature verification error: ${e.message}`,
216
+ };
217
+ }
218
+ }
113
219
  async function checkConditionHashes(results) {
114
220
  if (!subtle) {
115
221
  return { passed: false, reason: "Web Crypto API not available" };
@@ -157,31 +263,92 @@ function checkFreshness(results, maxAge) {
157
263
  }
158
264
  return { passed: true };
159
265
  }
160
- function checkExpiry(attestation) {
161
- const expiresAt = new Date(attestation.expiresAt).getTime();
162
- if (isNaN(expiresAt)) {
266
+ function checkExpiry(expiresAt) {
267
+ const ts = new Date(expiresAt).getTime();
268
+ if (isNaN(ts)) {
163
269
  return { passed: false, reason: "Invalid expiresAt timestamp" };
164
270
  }
165
- if (Date.now() > expiresAt) {
271
+ if (Date.now() > ts) {
166
272
  return { passed: false, reason: "Attestation has expired" };
167
273
  }
168
274
  return { passed: true };
169
275
  }
276
+ // ── JWT verification path ─────────────────────────────────────────
277
+ async function verifyJwt(token, options) {
278
+ let jwt;
279
+ try {
280
+ jwt = parseJwt(token);
281
+ }
282
+ catch (e) {
283
+ return {
284
+ valid: false,
285
+ checks: {
286
+ signature: { passed: false, reason: `JWT parse error: ${e.message}` },
287
+ conditionHashes: { passed: false, reason: "Skipped (JWT parse failed)" },
288
+ freshness: { passed: false, reason: "Skipped (JWT parse failed)" },
289
+ expiry: { passed: false, reason: "Skipped (JWT parse failed)" },
290
+ },
291
+ };
292
+ }
293
+ const p = jwt.payload;
294
+ // Resolve the signing key: use jwksUrl from options, or from JWT kid, or hardcoded
295
+ const jwksUrl = options?.jwksUrl || DEFAULT_JWKS_URL;
296
+ const kid = jwt.header.kid;
297
+ let keyJwk;
298
+ try {
299
+ keyJwk = await fetchJwksKey(jwksUrl, kid);
300
+ }
301
+ catch (e) {
302
+ return {
303
+ valid: false,
304
+ checks: {
305
+ signature: { passed: false, reason: `JWKS fetch error: ${e.message}` },
306
+ conditionHashes: { passed: false, reason: "Skipped (JWKS fetch failed)" },
307
+ freshness: { passed: false, reason: "Skipped (JWKS fetch failed)" },
308
+ expiry: { passed: false, reason: "Skipped (JWKS fetch failed)" },
309
+ },
310
+ };
311
+ }
312
+ // Extract attestation data from JWT claims
313
+ const results = (p.results || []);
314
+ // JWT exp → expiresAt ISO string
315
+ const expUnix = p.exp;
316
+ const expiresAt = expUnix ? new Date(expUnix * 1000).toISOString() : "";
317
+ const [signature, conditionHashes, freshness, expiry] = await Promise.all([
318
+ checkJwtSignature(jwt, keyJwk),
319
+ checkConditionHashes(results),
320
+ Promise.resolve(checkFreshness(results, options?.maxAge)),
321
+ Promise.resolve(checkExpiry(expiresAt)),
322
+ ]);
323
+ const valid = signature.passed &&
324
+ conditionHashes.passed &&
325
+ freshness.passed &&
326
+ expiry.passed;
327
+ return { valid, checks: { signature, conditionHashes, freshness, expiry } };
328
+ }
170
329
  // ── Main export ────────────────────────────────────────────────────
171
330
  /**
172
331
  * Verify an InsumerAPI attestation response.
173
332
  *
174
- * Runs 4 independent checks:
175
- * 1. **Signature** ECDSA P-256 over {id, pass, results, attestedAt}
333
+ * Auto-detects input format:
334
+ * - **String** JWT verification path (ES256 signature via JWKS)
335
+ * - **Object** → Raw attestation response path (ECDSA P1363 signature)
336
+ *
337
+ * Both formats run the same 4 checks:
338
+ * 1. **Signature** — ECDSA P-256 verification
176
339
  * 2. **Condition hashes** — SHA-256 of canonical sorted-key JSON
177
340
  * 3. **Freshness** — blockTimestamp age vs caller-defined maxAge (optional)
178
- * 4. **Expiry** — whether the 30-minute attestation window has elapsed
341
+ * 4. **Expiry** — whether the attestation window has elapsed
179
342
  *
180
- * @param response The full API response object (must contain data.attestation and data.sig)
181
- * @param options Optional configuration: maxAge (seconds) for freshness check
343
+ * @param response JWT string or full API response object
344
+ * @param options Optional configuration: maxAge (seconds), jwksUrl
182
345
  * @returns Structured result with overall validity and per-check details
183
346
  */
184
347
  export async function verifyAttestation(response, options) {
348
+ // Auto-detect: string → JWT, object → raw attestation
349
+ if (typeof response === "string") {
350
+ return verifyJwt(response, options);
351
+ }
185
352
  const parsed = parseResponse(response);
186
353
  const { attestation, sig, kid } = parsed.data;
187
354
  // If jwksUrl is provided, fetch the signing key dynamically
@@ -206,7 +373,7 @@ export async function verifyAttestation(response, options) {
206
373
  checkSignature(attestation, sig, keyJwk),
207
374
  checkConditionHashes(attestation.results),
208
375
  Promise.resolve(checkFreshness(attestation.results, options?.maxAge)),
209
- Promise.resolve(checkExpiry(attestation)),
376
+ Promise.resolve(checkExpiry(attestation.expiresAt)),
210
377
  ]);
211
378
  const valid = signature.passed &&
212
379
  conditionHashes.passed &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insumer-verify",
3
- "version": "1.1.4",
3
+ "version": "1.2.1",
4
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",