x402sgl 0.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 +166 -0
- package/node/x402layer-middleware.js +112 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<h1 align="center">Singularity SDK</h1>
|
|
3
|
+
<p align="center">
|
|
4
|
+
Server-side receipt verification middleware for the x402 payment protocol — Node.js & Python
|
|
5
|
+
</p>
|
|
6
|
+
<p align="center">
|
|
7
|
+
<a href="https://studio.x402layer.cc/docs/developer/sdk-receipts">Documentation</a>
|
|
8
|
+
·
|
|
9
|
+
<a href="https://api.x402layer.cc/.well-known/jwks.json">JWKS Endpoint</a>
|
|
10
|
+
·
|
|
11
|
+
<a href="https://studio.x402layer.cc">x402 Studio</a>
|
|
12
|
+
</p>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
When a buyer completes an x402 payment, the worker issues a **signed receipt JWT** containing the transaction details. This SDK provides middleware to cryptographically verify those receipts on your server using RS256 and JWKS — ensuring that only genuine, tamper-proof receipts grant access to your resources.
|
|
20
|
+
|
|
21
|
+
Both the Node and Python implementations handle JWKS key fetching, caching, signature verification, claim validation, and optional source-slug binding.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
### Node.js
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install github:ivaavimusic/Singularity-SDK
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const { verifyX402ReceiptToken, createX402ReceiptMiddleware } = require('@x402layer/sdk');
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Python
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install git+https://github.com/ivaavimusic/Singularity-SDK.git#subdirectory=python
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from x402layer_middleware import X402ReceiptVerifier, require_x402_receipt
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Python dependencies** (installed automatically): `PyJWT`, `cryptography`. FastAPI is optional — install with `pip install x402layer-sdk[fastapi]`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Node.js
|
|
52
|
+
|
|
53
|
+
### API
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
const {
|
|
57
|
+
verifyX402ReceiptToken,
|
|
58
|
+
createX402ReceiptMiddleware,
|
|
59
|
+
} = require('./x402layer-middleware');
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`verifyX402ReceiptToken(token, options?)`**
|
|
63
|
+
|
|
64
|
+
Verifies the receipt JWT and returns the decoded claims. Throws on invalid signature, expired token, or claim mismatch.
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
const claims = await verifyX402ReceiptToken(token, {
|
|
68
|
+
requiredSourceSlug: 'my-endpoint', // optional — prevents token replay
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**`createX402ReceiptMiddleware(options?)`**
|
|
73
|
+
|
|
74
|
+
Express middleware that reads the token from `X-X402-Receipt-Token` or `Authorization: Bearer`, verifies it, and attaches the claims to `req.x402Receipt`.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
app.get(
|
|
78
|
+
'/v1/resource',
|
|
79
|
+
createX402ReceiptMiddleware({ requiredSourceSlug: 'my-endpoint' }),
|
|
80
|
+
(req, res) => {
|
|
81
|
+
res.json({ data: '...', payer: req.x402Receipt.payer_wallet });
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Returns `401` if the token is missing or invalid, `403` if the source slug does not match.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Python
|
|
91
|
+
|
|
92
|
+
### API
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from x402layer_middleware import X402ReceiptVerifier, require_x402_receipt
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**`X402ReceiptVerifier`**
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
verifier = X402ReceiptVerifier(
|
|
102
|
+
jwks_url="https://api.x402layer.cc/.well-known/jwks.json", # default
|
|
103
|
+
issuer="https://api.x402layer.cc", # default
|
|
104
|
+
audience="x402layer:receipt", # default
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**`require_x402_receipt(verifier, required_source_slug?)`**
|
|
109
|
+
|
|
110
|
+
FastAPI dependency that reads the token from `X-X402-Receipt-Token` or `Authorization: Bearer`, verifies it, and returns the decoded claims.
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
@app.get("/v1/resource")
|
|
114
|
+
async def resource(receipt=require_x402_receipt(verifier, required_source_slug="my-endpoint")):
|
|
115
|
+
return {"payer": receipt["payer_wallet"], "amount": receipt["amount"]}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Raises `HTTP 401` if the token is missing or invalid, `HTTP 403` if the source slug does not match.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Receipt Claims
|
|
123
|
+
|
|
124
|
+
| Claim | Type | Description |
|
|
125
|
+
|-------|------|-------------|
|
|
126
|
+
| `event` | `string` | Always `"payment.succeeded"` |
|
|
127
|
+
| `source` | `string` | `"endpoint"` or `"product"` |
|
|
128
|
+
| `source_id` | `string` | UUID of the paid resource |
|
|
129
|
+
| `source_slug` | `string` | Slug of the resource |
|
|
130
|
+
| `amount` | `string` | Payment amount (e.g. `"1.00"`) |
|
|
131
|
+
| `currency` | `string` | Asset symbol (e.g. `"USDC"`) |
|
|
132
|
+
| `tx_hash` | `string` | On-chain transaction hash |
|
|
133
|
+
| `payer_wallet` | `string` | Buyer wallet address |
|
|
134
|
+
| `network` | `string` | `"base"` or `"solana"` |
|
|
135
|
+
| `status` | `string` | Settlement status |
|
|
136
|
+
| `iat` | `number` | Issued-at (Unix timestamp) |
|
|
137
|
+
| `exp` | `number` | Expiry (Unix timestamp) |
|
|
138
|
+
| `jti` | `string` | Unique receipt ID — use for idempotency |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Token Contract
|
|
143
|
+
|
|
144
|
+
| Property | Value |
|
|
145
|
+
|----------|-------|
|
|
146
|
+
| Format | JWT (JWS) |
|
|
147
|
+
| Algorithm | `RS256` |
|
|
148
|
+
| JWKS URL | `https://api.x402layer.cc/.well-known/jwks.json` |
|
|
149
|
+
| Issuer | `https://api.x402layer.cc` |
|
|
150
|
+
| Audience | `x402layer:receipt` |
|
|
151
|
+
|
|
152
|
+
The token is delivered in the `X-X402-Receipt-Token` response header after a successful payment.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Security
|
|
157
|
+
|
|
158
|
+
- **Always set `requiredSourceSlug`** in production to prevent receipt replay across resources.
|
|
159
|
+
- The SDK caches JWKS keys in memory (default 5-minute TTL) to avoid excessive network calls.
|
|
160
|
+
- To rotate signing keys: publish a new JWKS entry with a new `kid`, then update the worker private key. Tokens signed with old keys remain verifiable until they expire.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/*
|
|
2
|
+
x402layer receipt middleware (Node/Express)
|
|
3
|
+
- Verifies RS256 payment receipt JWTs issued by x402layer worker
|
|
4
|
+
- Uses JWKS from https://api.x402layer.cc/.well-known/jwks.json
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto')
|
|
8
|
+
|
|
9
|
+
const jwksCache = new Map()
|
|
10
|
+
|
|
11
|
+
function base64UrlDecode(value) {
|
|
12
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
13
|
+
const pad = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4))
|
|
14
|
+
return Buffer.from(normalized + pad, 'base64')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseJwt(token) {
|
|
18
|
+
const parts = token.split('.')
|
|
19
|
+
if (parts.length !== 3) throw new Error('Invalid JWT format')
|
|
20
|
+
const [headerB64, payloadB64, signatureB64] = parts
|
|
21
|
+
const header = JSON.parse(base64UrlDecode(headerB64).toString('utf8'))
|
|
22
|
+
const payload = JSON.parse(base64UrlDecode(payloadB64).toString('utf8'))
|
|
23
|
+
const signature = base64UrlDecode(signatureB64)
|
|
24
|
+
return { header, payload, signature, signingInput: `${headerB64}.${payloadB64}` }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchJwks(jwksUrl, cacheTtlMs = 5 * 60 * 1000) {
|
|
28
|
+
const cached = jwksCache.get(jwksUrl)
|
|
29
|
+
if (cached && Date.now() - cached.fetchedAt < cacheTtlMs) {
|
|
30
|
+
return cached.keys
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const res = await fetch(jwksUrl)
|
|
34
|
+
if (!res.ok) throw new Error(`Failed to fetch JWKS: ${res.status}`)
|
|
35
|
+
const json = await res.json()
|
|
36
|
+
if (!json.keys || !Array.isArray(json.keys)) throw new Error('Invalid JWKS payload')
|
|
37
|
+
|
|
38
|
+
jwksCache.set(jwksUrl, { keys: json.keys, fetchedAt: Date.now() })
|
|
39
|
+
return json.keys
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function verifyJwtSignature(parsedJwt, jwk) {
|
|
43
|
+
if (!jwk) throw new Error('Signing key not found')
|
|
44
|
+
if (jwk.kty !== 'RSA') throw new Error(`Unsupported key type: ${jwk.kty}`)
|
|
45
|
+
const publicKey = crypto.createPublicKey({ key: jwk, format: 'jwk' })
|
|
46
|
+
return crypto.verify(
|
|
47
|
+
'RSA-SHA256',
|
|
48
|
+
Buffer.from(parsedJwt.signingInput),
|
|
49
|
+
publicKey,
|
|
50
|
+
parsedJwt.signature
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function validateClaims(payload, options = {}) {
|
|
55
|
+
const now = Math.floor(Date.now() / 1000)
|
|
56
|
+
const issuer = options.issuer || 'https://api.x402layer.cc'
|
|
57
|
+
const audience = options.audience || 'x402layer:receipt'
|
|
58
|
+
|
|
59
|
+
if (payload.iss !== issuer) throw new Error('Invalid issuer')
|
|
60
|
+
if (payload.aud !== audience) throw new Error('Invalid audience')
|
|
61
|
+
if (!payload.exp || payload.exp < now) throw new Error('Receipt token expired')
|
|
62
|
+
if (payload.iat && payload.iat > now + 60) throw new Error('Invalid iat')
|
|
63
|
+
if (payload.event !== 'payment.succeeded') throw new Error('Invalid receipt event')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function verifyX402ReceiptToken(token, options = {}) {
|
|
67
|
+
const jwksUrl = options.jwksUrl || 'https://api.x402layer.cc/.well-known/jwks.json'
|
|
68
|
+
const parsed = parseJwt(token)
|
|
69
|
+
|
|
70
|
+
if (parsed.header.alg !== 'RS256') {
|
|
71
|
+
throw new Error(`Unsupported algorithm: ${parsed.header.alg}`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const keys = await fetchJwks(jwksUrl, options.cacheTtlMs)
|
|
75
|
+
const jwk = keys.find((key) => key.kid === parsed.header.kid) || keys[0]
|
|
76
|
+
const validSignature = verifyJwtSignature(parsed, jwk)
|
|
77
|
+
if (!validSignature) throw new Error('Invalid receipt signature')
|
|
78
|
+
|
|
79
|
+
validateClaims(parsed.payload, options)
|
|
80
|
+
return parsed.payload
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createX402ReceiptMiddleware(options = {}) {
|
|
84
|
+
return async function x402ReceiptMiddleware(req, res, next) {
|
|
85
|
+
try {
|
|
86
|
+
const headerToken = req.headers['x-x402-receipt-token']
|
|
87
|
+
const authHeader = req.headers.authorization || ''
|
|
88
|
+
const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null
|
|
89
|
+
const token = headerToken || bearerToken
|
|
90
|
+
|
|
91
|
+
if (!token) {
|
|
92
|
+
return res.status(401).json({ error: 'Missing receipt token' })
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const claims = await verifyX402ReceiptToken(token, options)
|
|
96
|
+
|
|
97
|
+
if (options.requiredSourceSlug && claims.source_slug !== options.requiredSourceSlug) {
|
|
98
|
+
return res.status(403).json({ error: 'Token source mismatch' })
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
req.x402Receipt = claims
|
|
102
|
+
return next()
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return res.status(401).json({ error: err.message || 'Invalid receipt token' })
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
verifyX402ReceiptToken,
|
|
111
|
+
createX402ReceiptMiddleware
|
|
112
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "x402sgl",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Server-side receipt verification middleware for the x402 payment protocol",
|
|
5
|
+
"main": "node/x402layer-middleware.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./node/x402layer-middleware.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"node/x402layer-middleware.js",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"x402",
|
|
15
|
+
"payment",
|
|
16
|
+
"receipt",
|
|
17
|
+
"jwt",
|
|
18
|
+
"jwks",
|
|
19
|
+
"middleware",
|
|
20
|
+
"express",
|
|
21
|
+
"crypto",
|
|
22
|
+
"usdc",
|
|
23
|
+
"base",
|
|
24
|
+
"solana"
|
|
25
|
+
],
|
|
26
|
+
"author": "x402 Studio",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/ivaavimusic/Singularity-SDK.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://studio.x402layer.cc/docs/developer/sdk-receipts",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|