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 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 &amp; Python
5
+ </p>
6
+ <p align="center">
7
+ <a href="https://studio.x402layer.cc/docs/developer/sdk-receipts">Documentation</a>
8
+ &nbsp;·&nbsp;
9
+ <a href="https://api.x402layer.cc/.well-known/jwks.json">JWKS Endpoint</a>
10
+ &nbsp;·&nbsp;
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
+ }