insumer-verify 1.3.10 → 1.4.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 +21 -2
- package/examples/README.md +58 -0
- package/examples/basic-attest.mjs +64 -0
- package/examples/express-gate.mjs +94 -0
- package/examples/jwt-format.mjs +78 -0
- package/examples/multi-condition.mjs +79 -0
- package/examples/verify-manual.mjs +154 -0
- package/examples/xrpl-trustline.mjs +100 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -133,8 +133,8 @@ XRPL attestation results use `ledgerIndex` (integer) and `ledgerHash` (string) i
|
|
|
133
133
|
"evaluatedCondition": {
|
|
134
134
|
"type": "token_balance",
|
|
135
135
|
"chainId": "xrpl",
|
|
136
|
-
"
|
|
137
|
-
"
|
|
136
|
+
"contractAddress": "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De",
|
|
137
|
+
"currency": "524C555344000000000000000000000000000000",
|
|
138
138
|
"operator": "gte",
|
|
139
139
|
"threshold": 100,
|
|
140
140
|
"decimals": 6
|
|
@@ -271,6 +271,25 @@ Each attestation result includes an `evaluatedCondition` object and a `condition
|
|
|
271
271
|
|
|
272
272
|
This ensures the condition that was actually evaluated on-chain can be independently verified by any third party.
|
|
273
273
|
|
|
274
|
+
## Examples
|
|
275
|
+
|
|
276
|
+
The [`examples/`](./examples/) directory contains runnable scripts covering common patterns:
|
|
277
|
+
|
|
278
|
+
| Example | What it shows |
|
|
279
|
+
|---------|--------------|
|
|
280
|
+
| [`basic-attest.mjs`](./examples/basic-attest.mjs) | Single token balance check + verification |
|
|
281
|
+
| [`multi-condition.mjs`](./examples/multi-condition.mjs) | Multiple conditions across different chains |
|
|
282
|
+
| [`jwt-format.mjs`](./examples/jwt-format.mjs) | JWT format for gateway integration (Kong, Nginx, Cloudflare Access) |
|
|
283
|
+
| [`verify-manual.mjs`](./examples/verify-manual.mjs) | DIY verification with Web Crypto — no library, proving the format is open |
|
|
284
|
+
| [`express-gate.mjs`](./examples/express-gate.mjs) | Express middleware: verify attestation before granting access |
|
|
285
|
+
| [`xrpl-trustline.mjs`](./examples/xrpl-trustline.mjs) | XRPL trust line tokens (RLUSD, USDC) |
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
INSUMER_API_KEY=insr_live_... node examples/basic-attest.mjs
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
The attestation format is an open standard — `verify-manual.mjs` demonstrates full verification using only the Web Crypto API with no dependencies. See the [State Attestation Spec](https://insumermodel.com/state-attestation-spec) for the complete format definition.
|
|
292
|
+
|
|
274
293
|
## Pricing
|
|
275
294
|
|
|
276
295
|
**Tiers:** Free (10 credits) | Pro $9/mo (10,000/day) | Enterprise $29/mo (100,000/day)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# insumer-verify examples
|
|
2
|
+
|
|
3
|
+
Runnable examples showing how to request, verify, and gate access with InsumerAPI attestations.
|
|
4
|
+
|
|
5
|
+
## Philosophy
|
|
6
|
+
|
|
7
|
+
InsumerAPI standardizes the **format and verification model** of eligibility attestations, not the underlying computation method. Any conforming implementation may use direct reads, proof systems, or delegated verification, but it must emit a privacy-preserving, signed, time-bounded attestation that can be independently verified by relying parties without real-time issuer communication.
|
|
8
|
+
|
|
9
|
+
What this means in practice: every example below produces or consumes the same signed attestation format. The verification side never needs to know *how* the attestation was produced — only that it conforms to the [State Attestation Spec](https://insumermodel.com/state-attestation-spec).
|
|
10
|
+
|
|
11
|
+
## Examples
|
|
12
|
+
|
|
13
|
+
| File | What it shows |
|
|
14
|
+
|------|--------------|
|
|
15
|
+
| `basic-attest.mjs` | Single token balance check + verification |
|
|
16
|
+
| `multi-condition.mjs` | Multiple conditions across different chains |
|
|
17
|
+
| `jwt-format.mjs` | Request JWT format for gateway integration (Kong, Nginx, Cloudflare Access) |
|
|
18
|
+
| `verify-manual.mjs` | DIY verification with Web Crypto — no library, proving the format is open |
|
|
19
|
+
| `express-gate.mjs` | Express middleware: verify attestation before granting access |
|
|
20
|
+
| `xrpl-trustline.mjs` | XRPL trust line tokens (RLUSD, USDC) with issuer addressing |
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install insumer-verify # for library examples
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Get a free API key:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
curl -s -X POST https://api.insumermodel.com/v1/keys/create \
|
|
32
|
+
-H "Content-Type: application/json" \
|
|
33
|
+
-d '{"email": "you@example.com", "appName": "examples"}' | jq .
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Set it as an environment variable:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
export INSUMER_API_KEY=insr_live_your_key_here
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Run
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node examples/basic-attest.mjs
|
|
46
|
+
node examples/verify-manual.mjs # no library needed
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Verification spec
|
|
50
|
+
|
|
51
|
+
All examples verify attestations against the [State Attestation Specification](https://insumermodel.com/state-attestation-spec). The spec defines:
|
|
52
|
+
|
|
53
|
+
- **Attestation format** — signed boolean assertions over on-chain state
|
|
54
|
+
- **Signing scheme** — ECDSA P-256 (ES256) with JWKS key distribution
|
|
55
|
+
- **Condition hashes** — SHA-256 tamper seals over evaluated predicates
|
|
56
|
+
- **Verification algorithm** — 4 checks any relying party can run offline
|
|
57
|
+
|
|
58
|
+
The format is the standard. The computation behind it is an implementation detail.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* basic-attest.mjs — Single token balance attestation + verification.
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the core flow:
|
|
5
|
+
* 1. Send conditions to InsumerAPI
|
|
6
|
+
* 2. Receive a signed attestation (boolean, not balance)
|
|
7
|
+
* 3. Verify it cryptographically — offline, no API callback needed
|
|
8
|
+
*
|
|
9
|
+
* Usage: INSUMER_API_KEY=insr_live_... node examples/basic-attest.mjs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { verifyAttestation } from "insumer-verify";
|
|
13
|
+
|
|
14
|
+
const API_KEY = process.env.INSUMER_API_KEY;
|
|
15
|
+
if (!API_KEY) {
|
|
16
|
+
console.error("Set INSUMER_API_KEY environment variable");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 1. Request an attestation — "does this wallet hold >= 1000 USDC on Ethereum?"
|
|
21
|
+
const res = await fetch("https://api.insumermodel.com/v1/attest", {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"X-API-Key": API_KEY,
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
|
29
|
+
conditions: [
|
|
30
|
+
{
|
|
31
|
+
type: "token_balance",
|
|
32
|
+
chainId: 1,
|
|
33
|
+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
34
|
+
threshold: 1000,
|
|
35
|
+
decimals: 6,
|
|
36
|
+
label: "USDC >= 1000 on Ethereum",
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const apiResponse = await res.json();
|
|
43
|
+
|
|
44
|
+
// Check for RPC failure (retryable, not a verification failure)
|
|
45
|
+
if (!apiResponse.ok && apiResponse.error?.code === "rpc_failure") {
|
|
46
|
+
console.log("RPC failure — retry after 2-5 seconds:", apiResponse.error.failedConditions);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Verify the attestation — signature, condition hashes, freshness, expiry
|
|
51
|
+
const result = await verifyAttestation(apiResponse, {
|
|
52
|
+
maxAge: 120, // reject if block data is older than 2 minutes
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
console.log("Verification:", result.valid ? "PASSED" : "FAILED");
|
|
56
|
+
console.log("Checks:", JSON.stringify(result.checks, null, 2));
|
|
57
|
+
|
|
58
|
+
if (result.valid) {
|
|
59
|
+
const { pass, results } = apiResponse.data.attestation;
|
|
60
|
+
console.log(`\nAttestation: ${pass ? "ALL MET" : "NOT ALL MET"}`);
|
|
61
|
+
for (const r of results) {
|
|
62
|
+
console.log(` ${r.label}: ${r.met ? "met" : "not met"} (block ${r.blockNumber})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* express-gate.mjs — Express middleware: verify attestation before granting access.
|
|
3
|
+
*
|
|
4
|
+
* Pattern: the client calls InsumerAPI, gets an attestation, and sends it
|
|
5
|
+
* to your server. Your server verifies it cryptographically — no API
|
|
6
|
+
* callback, no real-time issuer communication. The attestation is
|
|
7
|
+
* self-contained proof that the wallet meets the conditions.
|
|
8
|
+
*
|
|
9
|
+
* This is the relying party pattern: your server never touches the
|
|
10
|
+
* blockchain or the attestation issuer. It only needs the public key
|
|
11
|
+
* (via JWKS) and the verification algorithm (4 checks).
|
|
12
|
+
*
|
|
13
|
+
* Usage: node examples/express-gate.mjs
|
|
14
|
+
* Then: curl -X POST http://localhost:3000/api/premium \
|
|
15
|
+
* -H "Content-Type: application/json" \
|
|
16
|
+
* -d '{"attestation": <full API response or JWT string>}'
|
|
17
|
+
*
|
|
18
|
+
* Requires: npm install express insumer-verify
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import express from "express";
|
|
22
|
+
import { verifyAttestation } from "insumer-verify";
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
app.use(express.json());
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Middleware: require a valid InsumerAPI attestation.
|
|
29
|
+
* Accepts either a raw attestation object or a JWT string.
|
|
30
|
+
*/
|
|
31
|
+
function requireAttestation(options = {}) {
|
|
32
|
+
const { maxAge = 120 } = options;
|
|
33
|
+
|
|
34
|
+
return async (req, res, next) => {
|
|
35
|
+
const attestationData = req.body.attestation;
|
|
36
|
+
|
|
37
|
+
if (!attestationData) {
|
|
38
|
+
return res.status(401).json({ error: "Missing attestation" });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = await verifyAttestation(attestationData, { maxAge });
|
|
43
|
+
|
|
44
|
+
if (!result.valid) {
|
|
45
|
+
return res.status(403).json({
|
|
46
|
+
error: "Attestation verification failed",
|
|
47
|
+
checks: result.checks,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Attach verified attestation to request for downstream use
|
|
52
|
+
if (typeof attestationData === "string") {
|
|
53
|
+
// JWT — decode claims
|
|
54
|
+
const [, payloadB64] = attestationData.split(".");
|
|
55
|
+
req.attestation = JSON.parse(
|
|
56
|
+
Buffer.from(payloadB64, "base64url").toString()
|
|
57
|
+
);
|
|
58
|
+
} else {
|
|
59
|
+
req.attestation = attestationData.data.attestation;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
next();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return res.status(400).json({ error: "Invalid attestation format" });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Protected route — only accessible with a valid attestation
|
|
70
|
+
app.post("/api/premium", requireAttestation({ maxAge: 120 }), (req, res) => {
|
|
71
|
+
const { pass, results, id } = req.attestation;
|
|
72
|
+
|
|
73
|
+
if (!pass) {
|
|
74
|
+
return res.status(403).json({
|
|
75
|
+
error: "Conditions not met",
|
|
76
|
+
attestationId: id,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
res.json({
|
|
81
|
+
message: "Welcome, verified holder",
|
|
82
|
+
attestationId: id,
|
|
83
|
+
conditionsMet: results.filter((r) => r.met).map((r) => r.label),
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Health check
|
|
88
|
+
app.get("/health", (req, res) => res.json({ ok: true }));
|
|
89
|
+
|
|
90
|
+
const PORT = process.env.PORT || 3000;
|
|
91
|
+
app.listen(PORT, () => {
|
|
92
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
93
|
+
console.log("POST /api/premium with { attestation: <response or JWT> }");
|
|
94
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jwt-format.mjs — Request attestation as a JWT for gateway integration.
|
|
3
|
+
*
|
|
4
|
+
* Adding "format": "jwt" to the request returns a standard ES256 JWT
|
|
5
|
+
* alongside the raw attestation. The JWT is verifiable by any standard
|
|
6
|
+
* JWT library or gateway (Kong, Nginx, Cloudflare Access, AWS API Gateway)
|
|
7
|
+
* using the JWKS endpoint — no custom code needed on the gateway side.
|
|
8
|
+
*
|
|
9
|
+
* This is the bridge between InsumerAPI and existing auth infrastructure:
|
|
10
|
+
* the attestation format is standard, so it plugs into standard tools.
|
|
11
|
+
*
|
|
12
|
+
* Usage: INSUMER_API_KEY=insr_live_... node examples/jwt-format.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { verifyAttestation } from "insumer-verify";
|
|
16
|
+
|
|
17
|
+
const API_KEY = process.env.INSUMER_API_KEY;
|
|
18
|
+
if (!API_KEY) {
|
|
19
|
+
console.error("Set INSUMER_API_KEY environment variable");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Request JWT format
|
|
24
|
+
const res = await fetch("https://api.insumermodel.com/v1/attest", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"X-API-Key": API_KEY,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
|
32
|
+
conditions: [
|
|
33
|
+
{
|
|
34
|
+
type: "token_balance",
|
|
35
|
+
chainId: 1,
|
|
36
|
+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
37
|
+
threshold: 100,
|
|
38
|
+
decimals: 6,
|
|
39
|
+
label: "USDC >= 100 on Ethereum",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
format: "jwt",
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const apiResponse = await res.json();
|
|
47
|
+
|
|
48
|
+
if (!apiResponse.ok) {
|
|
49
|
+
console.error("API error:", apiResponse.error);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The JWT is in the response data
|
|
54
|
+
const jwt = apiResponse.data.jwt;
|
|
55
|
+
console.log("JWT token:", jwt);
|
|
56
|
+
console.log();
|
|
57
|
+
|
|
58
|
+
// Decode and inspect JWT claims (without verification, for display)
|
|
59
|
+
const [, payloadB64] = jwt.split(".");
|
|
60
|
+
const claims = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
|
|
61
|
+
console.log("JWT claims:");
|
|
62
|
+
console.log(` iss: ${claims.iss}`);
|
|
63
|
+
console.log(` sub: ${claims.sub} (wallet)`);
|
|
64
|
+
console.log(` jti: ${claims.jti} (attestation ID)`);
|
|
65
|
+
console.log(` pass: ${claims.pass}`);
|
|
66
|
+
console.log(` iat: ${new Date(claims.iat * 1000).toISOString()}`);
|
|
67
|
+
console.log(` exp: ${new Date(claims.exp * 1000).toISOString()}`);
|
|
68
|
+
console.log();
|
|
69
|
+
|
|
70
|
+
// Verify the JWT — insumer-verify auto-detects string input as JWT
|
|
71
|
+
const result = await verifyAttestation(jwt);
|
|
72
|
+
|
|
73
|
+
console.log("JWT verification:", result.valid ? "PASSED" : "FAILED");
|
|
74
|
+
console.log("Checks:", JSON.stringify(result.checks, null, 2));
|
|
75
|
+
|
|
76
|
+
// You can also verify the raw attestation from the same response
|
|
77
|
+
const rawResult = await verifyAttestation(apiResponse);
|
|
78
|
+
console.log("\nRaw attestation verification:", rawResult.valid ? "PASSED" : "FAILED");
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multi-condition.mjs — Multiple conditions across different chains.
|
|
3
|
+
*
|
|
4
|
+
* A single attestation request can check up to 10 conditions across any
|
|
5
|
+
* combination of the 32 supported chains. The response is one signed
|
|
6
|
+
* attestation covering all conditions — pass is true only if ALL are met.
|
|
7
|
+
*
|
|
8
|
+
* Usage: INSUMER_API_KEY=insr_live_... node examples/multi-condition.mjs
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { verifyAttestation } from "insumer-verify";
|
|
12
|
+
|
|
13
|
+
const API_KEY = process.env.INSUMER_API_KEY;
|
|
14
|
+
if (!API_KEY) {
|
|
15
|
+
console.error("Set INSUMER_API_KEY environment variable");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check multiple conditions in a single call:
|
|
20
|
+
// - USDC on Ethereum (EVM, chainId 1)
|
|
21
|
+
// - UNI governance token on Ethereum
|
|
22
|
+
// - USDC on Base (EVM, chainId 8453)
|
|
23
|
+
const res = await fetch("https://api.insumermodel.com/v1/attest", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
"X-API-Key": API_KEY,
|
|
28
|
+
},
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
|
31
|
+
conditions: [
|
|
32
|
+
{
|
|
33
|
+
type: "token_balance",
|
|
34
|
+
chainId: 1,
|
|
35
|
+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
36
|
+
threshold: 100,
|
|
37
|
+
decimals: 6,
|
|
38
|
+
label: "USDC >= 100 on Ethereum",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: "token_balance",
|
|
42
|
+
chainId: 1,
|
|
43
|
+
contractAddress: "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
|
|
44
|
+
threshold: 1,
|
|
45
|
+
decimals: 18,
|
|
46
|
+
label: "UNI >= 1 on Ethereum",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
type: "token_balance",
|
|
50
|
+
chainId: 8453,
|
|
51
|
+
contractAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
52
|
+
threshold: 50,
|
|
53
|
+
decimals: 6,
|
|
54
|
+
label: "USDC >= 50 on Base",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const apiResponse = await res.json();
|
|
61
|
+
|
|
62
|
+
if (!apiResponse.ok) {
|
|
63
|
+
console.error("API error:", apiResponse.error);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Verify once — covers all conditions in the attestation
|
|
68
|
+
const result = await verifyAttestation(apiResponse, { maxAge: 120 });
|
|
69
|
+
|
|
70
|
+
console.log("Verification:", result.valid ? "PASSED" : "FAILED");
|
|
71
|
+
|
|
72
|
+
const { attestation } = apiResponse.data;
|
|
73
|
+
console.log(`\nAttestation ${attestation.id}:`);
|
|
74
|
+
console.log(` Overall: ${attestation.pass ? "ALL MET" : "NOT ALL MET"} (${attestation.passCount} passed, ${attestation.failCount} failed)`);
|
|
75
|
+
console.log(` Expires: ${attestation.expiresAt}`);
|
|
76
|
+
|
|
77
|
+
for (const r of attestation.results) {
|
|
78
|
+
console.log(` [${r.met ? "PASS" : "FAIL"}] ${r.label} (chain ${r.chainId}, block ${r.blockNumber})`);
|
|
79
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-manual.mjs — DIY attestation verification with Web Crypto.
|
|
3
|
+
*
|
|
4
|
+
* No library. No dependencies. Just the Web Crypto API and the spec.
|
|
5
|
+
*
|
|
6
|
+
* This example proves that the attestation format is open and independently
|
|
7
|
+
* verifiable. You don't need insumer-verify, you don't need to trust any
|
|
8
|
+
* library — you can verify directly against the JWKS public key using
|
|
9
|
+
* standard cryptographic primitives available in every modern runtime.
|
|
10
|
+
*
|
|
11
|
+
* The protocol standardizes the format and verification model, not the
|
|
12
|
+
* implementation. This file IS a conforming verifier.
|
|
13
|
+
*
|
|
14
|
+
* Implements all 4 verification checks from the State Attestation Spec:
|
|
15
|
+
* 1. Signature — ECDSA P-256 over {id, pass, results, attestedAt}
|
|
16
|
+
* 2. Condition hashes — SHA-256 of canonical sorted-key JSON
|
|
17
|
+
* 3. Freshness — blockTimestamp age vs maxAge
|
|
18
|
+
* 4. Expiry — attestation validity window
|
|
19
|
+
*
|
|
20
|
+
* Usage: INSUMER_API_KEY=insr_live_... node examples/verify-manual.mjs
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const API_KEY = process.env.INSUMER_API_KEY;
|
|
24
|
+
if (!API_KEY) {
|
|
25
|
+
console.error("Set INSUMER_API_KEY environment variable");
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Step 1: Fetch the signing key from JWKS ──────────────────────
|
|
30
|
+
|
|
31
|
+
const jwksRes = await fetch("https://api.insumermodel.com/v1/jwks");
|
|
32
|
+
const jwks = await jwksRes.json();
|
|
33
|
+
const keyData = jwks.keys[0]; // or match by kid
|
|
34
|
+
|
|
35
|
+
const publicKey = await crypto.subtle.importKey(
|
|
36
|
+
"jwk",
|
|
37
|
+
{ kty: keyData.kty, crv: keyData.crv, x: keyData.x, y: keyData.y },
|
|
38
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
39
|
+
false,
|
|
40
|
+
["verify"]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
console.log("Imported public key (kid:", keyData.kid, ")");
|
|
44
|
+
|
|
45
|
+
// ── Step 2: Request an attestation ───────────────────────────────
|
|
46
|
+
|
|
47
|
+
const res = await fetch("https://api.insumermodel.com/v1/attest", {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: {
|
|
50
|
+
"Content-Type": "application/json",
|
|
51
|
+
"X-API-Key": API_KEY,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
wallet: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
|
|
55
|
+
conditions: [
|
|
56
|
+
{
|
|
57
|
+
type: "token_balance",
|
|
58
|
+
chainId: 1,
|
|
59
|
+
contractAddress: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
|
60
|
+
threshold: 100,
|
|
61
|
+
decimals: 6,
|
|
62
|
+
label: "USDC >= 100 on Ethereum",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const apiResponse = await res.json();
|
|
69
|
+
if (!apiResponse.ok) {
|
|
70
|
+
console.error("API error:", apiResponse.error);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { attestation, sig, kid } = apiResponse.data;
|
|
75
|
+
console.log("Received attestation:", attestation.id);
|
|
76
|
+
|
|
77
|
+
// ── Check 1: Signature verification ─────────────────────────────
|
|
78
|
+
// The signed payload is JSON.stringify({id, pass, results, attestedAt})
|
|
79
|
+
// in that exact field order.
|
|
80
|
+
|
|
81
|
+
const signedPayload = JSON.stringify({
|
|
82
|
+
id: attestation.id,
|
|
83
|
+
pass: attestation.pass,
|
|
84
|
+
results: attestation.results,
|
|
85
|
+
attestedAt: attestation.attestedAt,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const payloadBytes = new TextEncoder().encode(signedPayload);
|
|
89
|
+
const sigBytes = Uint8Array.from(atob(sig), (c) => c.charCodeAt(0));
|
|
90
|
+
|
|
91
|
+
const sigValid = await crypto.subtle.verify(
|
|
92
|
+
{ name: "ECDSA", hash: "SHA-256" },
|
|
93
|
+
publicKey,
|
|
94
|
+
sigBytes,
|
|
95
|
+
payloadBytes
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
console.log("\nCheck 1 — Signature:", sigValid ? "PASSED" : "FAILED");
|
|
99
|
+
|
|
100
|
+
// ── Check 2: Condition hash integrity ────────────────────────────
|
|
101
|
+
// Each result's conditionHash must equal SHA-256 of its evaluatedCondition
|
|
102
|
+
// serialized with sorted keys.
|
|
103
|
+
|
|
104
|
+
let hashesValid = true;
|
|
105
|
+
for (const r of attestation.results) {
|
|
106
|
+
const sortedKeys = Object.keys(r.evaluatedCondition).sort();
|
|
107
|
+
const canonical = JSON.stringify(r.evaluatedCondition, sortedKeys);
|
|
108
|
+
const hashBuffer = await crypto.subtle.digest(
|
|
109
|
+
"SHA-256",
|
|
110
|
+
new TextEncoder().encode(canonical)
|
|
111
|
+
);
|
|
112
|
+
const computed =
|
|
113
|
+
"0x" +
|
|
114
|
+
Array.from(new Uint8Array(hashBuffer))
|
|
115
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
116
|
+
.join("");
|
|
117
|
+
|
|
118
|
+
if (computed !== r.conditionHash) {
|
|
119
|
+
console.log(`Check 2 — Condition hash MISMATCH at index ${r.condition}`);
|
|
120
|
+
console.log(` Expected: ${r.conditionHash}`);
|
|
121
|
+
console.log(` Computed: ${computed}`);
|
|
122
|
+
hashesValid = false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
console.log("Check 2 — Condition hashes:", hashesValid ? "PASSED" : "FAILED");
|
|
126
|
+
|
|
127
|
+
// ── Check 3: Freshness ───────────────────────────────────────────
|
|
128
|
+
// Reject if blockTimestamp is older than maxAge seconds.
|
|
129
|
+
|
|
130
|
+
const MAX_AGE_SECONDS = 120;
|
|
131
|
+
let freshnessValid = true;
|
|
132
|
+
for (const r of attestation.results) {
|
|
133
|
+
if (!r.blockTimestamp) continue; // skip chains without block timestamps
|
|
134
|
+
const ageMs = Date.now() - new Date(r.blockTimestamp).getTime();
|
|
135
|
+
if (ageMs > MAX_AGE_SECONDS * 1000) {
|
|
136
|
+
console.log(
|
|
137
|
+
`Check 3 — Result ${r.condition} is ${Math.round(ageMs / 1000)}s old (max: ${MAX_AGE_SECONDS}s)`
|
|
138
|
+
);
|
|
139
|
+
freshnessValid = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
console.log("Check 3 — Freshness:", freshnessValid ? "PASSED" : "FAILED");
|
|
143
|
+
|
|
144
|
+
// ── Check 4: Expiry ──────────────────────────────────────────────
|
|
145
|
+
// Reject if the attestation's validity window has elapsed.
|
|
146
|
+
|
|
147
|
+
const expiryValid = Date.now() <= new Date(attestation.expiresAt).getTime();
|
|
148
|
+
console.log("Check 4 — Expiry:", expiryValid ? "PASSED" : "FAILED");
|
|
149
|
+
|
|
150
|
+
// ── Summary ──────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const allPassed = sigValid && hashesValid && freshnessValid && expiryValid;
|
|
153
|
+
console.log("\nOverall:", allPassed ? "VALID" : "INVALID");
|
|
154
|
+
console.log(`Attestation ${attestation.id}: ${attestation.pass ? "conditions met" : "conditions not met"}`);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xrpl-trustline.mjs — XRPL trust line token attestation.
|
|
3
|
+
*
|
|
4
|
+
* XRPL uses a different addressing model than EVM chains:
|
|
5
|
+
* - Tokens are identified by issuer address + currency code
|
|
6
|
+
* - The contractAddress field holds the issuer's r-address
|
|
7
|
+
* - Results include ledgerIndex/ledgerHash instead of blockNumber/blockTimestamp
|
|
8
|
+
* - Trust line tokens include trustLineState with frozen status
|
|
9
|
+
*
|
|
10
|
+
* The attestation format is the same — signed, verifiable, boolean.
|
|
11
|
+
* The chain-specific details are in the evaluatedCondition and result fields.
|
|
12
|
+
*
|
|
13
|
+
* Usage: INSUMER_API_KEY=insr_live_... node examples/xrpl-trustline.mjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { verifyAttestation } from "insumer-verify";
|
|
17
|
+
|
|
18
|
+
const API_KEY = process.env.INSUMER_API_KEY;
|
|
19
|
+
if (!API_KEY) {
|
|
20
|
+
console.error("Set INSUMER_API_KEY environment variable");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// XRPL token issuers and currency codes
|
|
25
|
+
const RLUSD_ISSUER = "rMxCKbEDwqr76QuheSUMdEGf4B9xJ8m5De";
|
|
26
|
+
const RLUSD_CURRENCY = "524C555344000000000000000000000000000000";
|
|
27
|
+
|
|
28
|
+
const USDC_ISSUER = "rGm7WCVp9gb4jZHWTEtGUr4dd74z2XuWhE";
|
|
29
|
+
const USDC_CURRENCY = "5553444300000000000000000000000000000000";
|
|
30
|
+
|
|
31
|
+
// Check RLUSD and USDC on XRPL
|
|
32
|
+
// Note: XRPL uses xrplWallet parameter (not wallet, which is EVM)
|
|
33
|
+
const res = await fetch("https://api.insumermodel.com/v1/attest", {
|
|
34
|
+
method: "POST",
|
|
35
|
+
headers: {
|
|
36
|
+
"Content-Type": "application/json",
|
|
37
|
+
"X-API-Key": API_KEY,
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
xrplWallet: "ra8xqX4QhcogFfxpMxMByvFnXyxw9E8rzY",
|
|
41
|
+
conditions: [
|
|
42
|
+
{
|
|
43
|
+
type: "token_balance",
|
|
44
|
+
chainId: "xrpl",
|
|
45
|
+
contractAddress: RLUSD_ISSUER,
|
|
46
|
+
currency: RLUSD_CURRENCY,
|
|
47
|
+
threshold: 10,
|
|
48
|
+
decimals: 6,
|
|
49
|
+
label: "RLUSD >= 10 on XRPL",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
type: "token_balance",
|
|
53
|
+
chainId: "xrpl",
|
|
54
|
+
contractAddress: USDC_ISSUER,
|
|
55
|
+
currency: USDC_CURRENCY,
|
|
56
|
+
threshold: 5,
|
|
57
|
+
decimals: 6,
|
|
58
|
+
label: "USDC >= 5 on XRPL",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const apiResponse = await res.json();
|
|
65
|
+
|
|
66
|
+
if (!apiResponse.ok) {
|
|
67
|
+
console.error("API error:", apiResponse.error);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Verify — same function, same checks. XRPL results use ledgerIndex
|
|
72
|
+
// instead of blockTimestamp, so freshness check skips them gracefully.
|
|
73
|
+
const result = await verifyAttestation(apiResponse, { maxAge: 120 });
|
|
74
|
+
|
|
75
|
+
console.log("Verification:", result.valid ? "PASSED" : "FAILED");
|
|
76
|
+
|
|
77
|
+
const { attestation } = apiResponse.data;
|
|
78
|
+
console.log(`\nAttestation ${attestation.id}:`);
|
|
79
|
+
|
|
80
|
+
for (const r of attestation.results) {
|
|
81
|
+
console.log(`\n ${r.label}: ${r.met ? "MET" : "NOT MET"}`);
|
|
82
|
+
console.log(` Chain: ${r.chainId}`);
|
|
83
|
+
|
|
84
|
+
// XRPL-specific fields
|
|
85
|
+
if (r.ledgerIndex) {
|
|
86
|
+
console.log(` Ledger index: ${r.ledgerIndex}`);
|
|
87
|
+
}
|
|
88
|
+
if (r.ledgerHash) {
|
|
89
|
+
console.log(` Ledger hash: ${r.ledgerHash}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Trust line state (non-native tokens only)
|
|
93
|
+
if (r.trustLineState) {
|
|
94
|
+
console.log(` Trust line frozen: ${r.trustLineState.frozen}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The evaluatedCondition uses contractAddress for the issuer
|
|
98
|
+
console.log(` Issuer (contractAddress): ${r.evaluatedCondition.contractAddress}`);
|
|
99
|
+
console.log(` Currency: ${r.evaluatedCondition.currency}`);
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insumer-verify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"build",
|
|
16
|
+
"examples",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|