insumer-verify 1.0.0 → 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 +15 -2
- package/build/index.d.ts +2 -0
- package/build/index.js +39 -5
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install insumer-verify
|
|
|
16
16
|
import { verifyAttestation } from "insumer-verify";
|
|
17
17
|
|
|
18
18
|
// Call InsumerAPI
|
|
19
|
-
const res = await fetch("https://
|
|
19
|
+
const res = await fetch("https://us-central1-insumer-merchant.cloudfunctions.net/insumerApi/v1/attest", {
|
|
20
20
|
method: "POST",
|
|
21
21
|
headers: {
|
|
22
22
|
"Content-Type": "application/json",
|
|
@@ -54,7 +54,7 @@ if (result.valid) {
|
|
|
54
54
|
<script type="module">
|
|
55
55
|
import { verifyAttestation } from "https://esm.sh/insumer-verify";
|
|
56
56
|
|
|
57
|
-
const res = await fetch("https://
|
|
57
|
+
const res = await fetch("https://us-central1-insumer-merchant.cloudfunctions.net/insumerApi/v1/attest", {
|
|
58
58
|
method: "POST",
|
|
59
59
|
headers: {
|
|
60
60
|
"Content-Type": "application/json",
|
|
@@ -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
|
-
|
|
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.
|
|
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",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"build"
|
|
15
|
+
"build",
|
|
16
|
+
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"build": "tsc",
|