mirror of
https://github.com/Permissionless-Software-Foundation/psf-bch-api-base.git
synced 2026-09-21 16:52:00 -07:00
feat(x402): Added x402 protection for each endpoint
This commit is contained in:
@@ -6,3 +6,25 @@ This is a REST API for communicating with Bitcoin Cash infrastructure. It replac
|
||||
|
||||
[MIT](./LICENSE.md)
|
||||
|
||||
## x402-bch Payments
|
||||
|
||||
All REST endpoints exposed under the `/v6` prefix are protected by the [`x402-bch-express`](https://www.npmjs.com/package/x402-bch-express) middleware. Each API call requires a BCH payment authorization for **2000 satoshis**. The middleware advertises payment requirements via HTTP 402 responses and validates incoming `X-PAYMENT` headers with a configured Facilitator.
|
||||
|
||||
### Configuration
|
||||
|
||||
Environment variables control the payment flow:
|
||||
|
||||
- `X402_ENABLED` — set to `false` (case-insensitive) to disable the middleware. Defaults to enabled.
|
||||
- `SERVER_BCH_ADDRESS` — BCH cash address that receives funding transactions. Defaults to `bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d`.
|
||||
- `FACILITATOR_URL` — Root URL of the facilitator service (e.g., `http://localhost:4345/facilitator`).
|
||||
- `X402_PRICE_SAT` — Optional; override the satoshi price per call (defaults to `2000`).
|
||||
|
||||
When `X402_ENABLED=false`, the server continues to operate without payment headers for local development or trusted deployments.
|
||||
|
||||
### Manual Verification
|
||||
|
||||
1. Start or point to an `x402-bch` facilitator service (the example facilitator listens at `http://localhost:4345/facilitator`).
|
||||
2. Run the API server with the default configuration: `npm start`.
|
||||
3. Call a protected endpoint without an `X-PAYMENT` header, e.g. `curl -i http://localhost:5942/v6/full-node/control/getNetworkInfo`. The server will respond with HTTP `402` and include payment requirements.
|
||||
4. Restart the server with `X402_ENABLED=false npm start` to confirm that the same request now bypasses the middleware (useful for local development without payments).
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import express from 'express'
|
||||
import cors from 'cors'
|
||||
import dotenv from 'dotenv'
|
||||
import { paymentMiddleware as x402PaymentMiddleware } from 'x402-bch-express'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
@@ -14,6 +15,7 @@ import { dirname, join } from 'path'
|
||||
import config from '../src/config/index.js'
|
||||
import Controllers from '../src/controllers/index.js'
|
||||
import wlogger from '../src/adapters/wlogger.js'
|
||||
import { buildX402Routes, getX402Settings } from '../src/config/x402.js'
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config()
|
||||
@@ -57,6 +59,8 @@ class Server {
|
||||
// Create an Express instance.
|
||||
const app = express()
|
||||
|
||||
const x402Settings = getX402Settings()
|
||||
|
||||
// MIDDLEWARE START
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
@@ -68,6 +72,24 @@ class Server {
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
|
||||
}))
|
||||
|
||||
if (x402Settings.enabled) {
|
||||
const routes = buildX402Routes(this.config.apiPrefix)
|
||||
const facilitatorOptions = x402Settings.facilitatorUrl
|
||||
? { url: x402Settings.facilitatorUrl }
|
||||
: undefined
|
||||
|
||||
wlogger.info(`x402 middleware enabled; enforcing ${x402Settings.priceSat} satoshis per request`)
|
||||
app.use(
|
||||
x402PaymentMiddleware(
|
||||
x402Settings.serverAddress,
|
||||
routes,
|
||||
facilitatorOptions
|
||||
)
|
||||
)
|
||||
} else {
|
||||
wlogger.info('x402 middleware disabled via configuration')
|
||||
}
|
||||
|
||||
// Endpoint logging middleware
|
||||
app.use((req, res, next) => {
|
||||
console.log(`Endpoint called: ${req.method} ${req.path}`)
|
||||
|
||||
Generated
+1043
-83
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -20,7 +20,8 @@
|
||||
"dotenv": "16.3.1",
|
||||
"express": "5.1.0",
|
||||
"winston": "3.11.0",
|
||||
"winston-daily-rotate-file": "4.7.1"
|
||||
"winston-daily-rotate-file": "4.7.1",
|
||||
"x402-bch-express": "1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"apidoc": "1.2.0",
|
||||
|
||||
Vendored
+24
@@ -16,6 +16,25 @@ const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../../packag
|
||||
|
||||
const version = pkgInfo.version
|
||||
|
||||
const normalizeBoolean = (value, defaultValue) => {
|
||||
if (value === undefined || value === null || value === '') return defaultValue
|
||||
|
||||
const normalized = String(value).trim().toLowerCase()
|
||||
if (['false', '0', 'no', 'off'].includes(normalized)) return false
|
||||
if (['true', '1', 'yes', 'on'].includes(normalized)) return true
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const parsedPriceSat = Number(process.env.X402_PRICE_SAT)
|
||||
const priceSat = Number.isFinite(parsedPriceSat) && parsedPriceSat > 0 ? parsedPriceSat : 2000
|
||||
|
||||
const x402Defaults = {
|
||||
enabled: normalizeBoolean(process.env.X402_ENABLED, true),
|
||||
facilitatorUrl: process.env.FACILITATOR_URL || 'http://localhost:4345/facilitator',
|
||||
serverAddress: process.env.SERVER_BCH_ADDRESS || 'bitcoincash:qqlrzp23w08434twmvr4fxw672whkjy0py26r63g3d',
|
||||
priceSat
|
||||
}
|
||||
|
||||
export default {
|
||||
// Server port
|
||||
port: process.env.PORT || 5942,
|
||||
@@ -23,6 +42,9 @@ export default {
|
||||
// Environment
|
||||
env: process.env.NODE_ENV || 'development',
|
||||
|
||||
// API prefix for REST controllers
|
||||
apiPrefix: process.env.API_PREFIX || '/v6',
|
||||
|
||||
// Logging level
|
||||
logLevel: process.env.LOG_LEVEL || 'info',
|
||||
|
||||
@@ -59,6 +81,8 @@ export default {
|
||||
rpcRequestIdPrefix: process.env.RPC_REQUEST_ID_PREFIX || 'psf-bch-api'
|
||||
},
|
||||
|
||||
x402: x402Defaults,
|
||||
|
||||
// Version
|
||||
version
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import config from './index.js'
|
||||
|
||||
const DEFAULT_DESCRIPTION = 'Access to protected psf-bch-api resources'
|
||||
const DEFAULT_TIMEOUT_SECONDS = 60
|
||||
const NETWORK = 'bch'
|
||||
|
||||
/**
|
||||
* Builds a route configuration map for x402-bch middleware.
|
||||
*
|
||||
* @param {string} apiPrefix Express API prefix (e.g., "/v6")
|
||||
* @returns {Object} Routes configuration compatible with x402-bch-express
|
||||
*/
|
||||
export function buildX402Routes (apiPrefix = '/v6') {
|
||||
const normalizedPrefix = apiPrefix.endsWith('/')
|
||||
? apiPrefix.slice(0, -1)
|
||||
: apiPrefix
|
||||
const prefixWithSlash = normalizedPrefix.startsWith('/')
|
||||
? normalizedPrefix
|
||||
: `/${normalizedPrefix}`
|
||||
|
||||
const routeKey = `${prefixWithSlash}/*`
|
||||
|
||||
return {
|
||||
network: NETWORK,
|
||||
[routeKey]: {
|
||||
price: config.x402.priceSat,
|
||||
network: NETWORK,
|
||||
config: {
|
||||
description: `${DEFAULT_DESCRIPTION} (2000 satoshis)`,
|
||||
maxTimeoutSeconds: DEFAULT_TIMEOUT_SECONDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getX402Settings () {
|
||||
return {
|
||||
enabled: Boolean(config.x402?.enabled),
|
||||
facilitatorUrl: config.x402?.facilitatorUrl,
|
||||
serverAddress: config.x402?.serverAddress,
|
||||
priceSat: config.x402?.priceSat
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class Controllers {
|
||||
this.useCases = new UseCases({ adapters: this.adapters })
|
||||
this.config = config
|
||||
this.timerController = new TimerController({ adapters: this.adapters, useCases: this.useCases })
|
||||
this.apiPrefix = this.config.apiPrefix || '/v6'
|
||||
|
||||
// Bind 'this' object to all subfunctions
|
||||
this.initAdapters = this.initAdapters.bind(this)
|
||||
@@ -45,7 +46,8 @@ class Controllers {
|
||||
attachRESTControllers (app) {
|
||||
const restControllers = new RESTControllers({
|
||||
adapters: this.adapters,
|
||||
useCases: this.useCases
|
||||
useCases: this.useCases,
|
||||
apiPrefix: this.apiPrefix
|
||||
})
|
||||
|
||||
// Attach the REST API Controllers to the Express app.
|
||||
|
||||
Reference in New Issue
Block a user