insumer-verify 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -91,6 +91,18 @@ const result = await verifyAttestation(apiResponse, { maxAge: 120 });
91
91
 
92
92
  Results without `blockTimestamp` (Covalent and Solana chains) are skipped, not treated as failures.
93
93
 
94
+ ### JWKS key discovery
95
+
96
+ By default, insumer-verify uses the hardcoded InsumerAPI public key. You can opt in to dynamic key discovery via the JWKS endpoint:
97
+
98
+ ```typescript
99
+ const result = await verifyAttestation(apiResponse, {
100
+ jwksUrl: "https://insumermodel.com/.well-known/jwks.json"
101
+ });
102
+ ```
103
+
104
+ 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.
105
+
94
106
  ## API
95
107
 
96
108
  ### `verifyAttestation(response, options?)`
@@ -99,6 +111,7 @@ Results without `blockTimestamp` (Covalent and Solana chains) are skipped, not t
99
111
  |-----------|------|-------------|
100
112
  | `response` | `unknown` | Full InsumerAPI response (must contain `data.attestation` and `data.sig`) |
101
113
  | `options.maxAge` | `number` | Optional max age in seconds for block freshness check |
114
+ | `options.jwksUrl` | `string` | Optional JWKS URL for dynamic key discovery (e.g. `https://insumermodel.com/.well-known/jwks.json`) |
102
115
 
103
116
  Returns `Promise<VerifyResult>`:
104
117
 
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
- return { data: { attestation, sig } };
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,6 +1,6 @@
1
1
  {
2
2
  "name": "insumer-verify",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Reference verifier for InsumerAPI attestations. Validates ECDSA signatures, condition hashes, block freshness, and expiry. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "build/index.js",