@armory-sh/middleware-express 0.4.5 → 0.4.7
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 +77 -0
- package/package.json +3 -2
- package/src/index.ts +4 -9
- package/src/payment-utils.ts +0 -3
- package/src/routes.ts +155 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @armory-sh/middleware-express
|
|
2
|
+
|
|
3
|
+
x402 payment middleware for Express applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @armory-sh/middleware-express
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Simple payment middleware for Express
|
|
14
|
+
- Route-aware payment configuration
|
|
15
|
+
- Multi-network, multi-token support
|
|
16
|
+
- Full TypeScript support
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import express from "express";
|
|
22
|
+
import { paymentMiddleware } from "@armory-sh/middleware-express";
|
|
23
|
+
|
|
24
|
+
const app = express();
|
|
25
|
+
|
|
26
|
+
const requirements = {
|
|
27
|
+
scheme: "exact" as const,
|
|
28
|
+
network: "eip155:8453",
|
|
29
|
+
amount: "1000000",
|
|
30
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`,
|
|
31
|
+
payTo: "0xYourAddress..." as `0x${string}`,
|
|
32
|
+
maxTimeoutSeconds: 300,
|
|
33
|
+
extra: {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
app.use(paymentMiddleware({ requirements }));
|
|
37
|
+
|
|
38
|
+
app.get("/api/data", (req, res) => {
|
|
39
|
+
const payment = (req as AugmentedRequest).payment;
|
|
40
|
+
res.json({
|
|
41
|
+
data: "protected data",
|
|
42
|
+
payerAddress: payment?.payerAddress,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Route-Aware Middleware
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { routeAwarePaymentMiddleware } from "@armory-sh/middleware-express";
|
|
51
|
+
import type { PaymentRequirementsV2 } from "@armory-sh/base";
|
|
52
|
+
|
|
53
|
+
const premiumRequirements: PaymentRequirementsV2 = {
|
|
54
|
+
scheme: "exact",
|
|
55
|
+
network: "eip155:8453",
|
|
56
|
+
amount: "5000000",
|
|
57
|
+
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`,
|
|
58
|
+
payTo: "0xYourAddress..." as `0x${string}`,
|
|
59
|
+
maxTimeoutSeconds: 300,
|
|
60
|
+
extra: {},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
app.use("/api/premium", routeAwarePaymentMiddleware({
|
|
64
|
+
"/api/premium": { requirements: premiumRequirements },
|
|
65
|
+
"/api/basic": { requirements: basicRequirements },
|
|
66
|
+
}));
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
### `paymentMiddleware(config)`
|
|
72
|
+
|
|
73
|
+
Creates a payment middleware for Express.
|
|
74
|
+
|
|
75
|
+
### `routeAwarePaymentMiddleware(perRouteConfig)`
|
|
76
|
+
|
|
77
|
+
Creates a route-aware payment middleware for Express. Takes a record mapping route patterns to payment configuration entries.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@armory-sh/middleware-express",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
|
|
6
6
|
"type": "module",
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
"express": "^5"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@armory-sh/base": "0.2.
|
|
30
|
+
"@armory-sh/base": "0.2.20"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"bun-types": "latest",
|
|
34
|
+
"tsup": "^8.5.1",
|
|
34
35
|
"typescript": "5.9.3",
|
|
35
36
|
"express": "^5",
|
|
36
37
|
"@types/express": "^5"
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
export interface PaymentMiddlewareConfig {
|
|
14
14
|
requirements: PaymentRequirements;
|
|
15
15
|
facilitatorUrl?: string;
|
|
16
|
-
skipVerification?: boolean;
|
|
17
16
|
network?: string;
|
|
18
17
|
}
|
|
19
18
|
|
|
@@ -26,7 +25,7 @@ export interface AugmentedRequest extends Request {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export const paymentMiddleware = (config: PaymentMiddlewareConfig) => {
|
|
29
|
-
const { requirements, facilitatorUrl,
|
|
28
|
+
const { requirements, facilitatorUrl, network = "base" } = config;
|
|
30
29
|
|
|
31
30
|
return async (req: AugmentedRequest, res: Response, next: NextFunction): Promise<void> => {
|
|
32
31
|
try {
|
|
@@ -59,12 +58,6 @@ export const paymentMiddleware = (config: PaymentMiddlewareConfig) => {
|
|
|
59
58
|
|
|
60
59
|
const payerAddress = paymentPayload.payload.authorization.from;
|
|
61
60
|
|
|
62
|
-
// TODO: Add verification with facilitator if not skipVerification
|
|
63
|
-
// if (!skipVerification && facilitatorUrl) {
|
|
64
|
-
// const result = await verifyWithFacilitator(...);
|
|
65
|
-
// if (!result.success) { ... }
|
|
66
|
-
// }
|
|
67
|
-
|
|
68
61
|
const settlement = {
|
|
69
62
|
success: true,
|
|
70
63
|
transaction: "",
|
|
@@ -76,7 +69,7 @@ export const paymentMiddleware = (config: PaymentMiddlewareConfig) => {
|
|
|
76
69
|
res.setHeader(key, value);
|
|
77
70
|
}
|
|
78
71
|
|
|
79
|
-
req.payment = { payload: paymentPayload, payerAddress, verified:
|
|
72
|
+
req.payment = { payload: paymentPayload, payerAddress, verified: true };
|
|
80
73
|
next();
|
|
81
74
|
} catch (error) {
|
|
82
75
|
res.statusCode = 500;
|
|
@@ -87,3 +80,5 @@ export const paymentMiddleware = (config: PaymentMiddlewareConfig) => {
|
|
|
87
80
|
}
|
|
88
81
|
};
|
|
89
82
|
};
|
|
83
|
+
|
|
84
|
+
export { routeAwarePaymentMiddleware, type RouteAwarePaymentMiddlewareConfig, type PaymentMiddlewareConfigEntry } from "./routes";
|
package/src/payment-utils.ts
CHANGED
|
@@ -74,7 +74,6 @@ export const decodePayload = (
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Check for x402 V2 format
|
|
78
77
|
if (isPaymentPayload(payload)) {
|
|
79
78
|
return { payload };
|
|
80
79
|
}
|
|
@@ -87,7 +86,6 @@ export const decodePayload = (
|
|
|
87
86
|
};
|
|
88
87
|
|
|
89
88
|
export const extractPayerAddress = (payload: AnyPaymentPayload): string => {
|
|
90
|
-
// Check for x402 format with nested payload
|
|
91
89
|
if (isPaymentPayload(payload)) {
|
|
92
90
|
const p = payload.payload;
|
|
93
91
|
if (isExactEvmPayload(p) && p.authorization?.from) {
|
|
@@ -95,7 +93,6 @@ export const extractPayerAddress = (payload: AnyPaymentPayload): string => {
|
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
// Check for direct `from` field (legacy format)
|
|
99
96
|
if ("from" in payload && typeof payload.from === "string") {
|
|
100
97
|
return payload.from;
|
|
101
98
|
}
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
import type {
|
|
3
|
+
PaymentPayloadV2,
|
|
4
|
+
PaymentRequirements,
|
|
5
|
+
} from "@armory-sh/base";
|
|
6
|
+
import {
|
|
7
|
+
decodePaymentV2,
|
|
8
|
+
createPaymentRequiredHeaders,
|
|
9
|
+
createSettlementHeaders,
|
|
10
|
+
PAYMENT_SIGNATURE_HEADER,
|
|
11
|
+
matchRoute,
|
|
12
|
+
validateRouteConfig,
|
|
13
|
+
type RouteValidationError,
|
|
14
|
+
} from "@armory-sh/base";
|
|
15
|
+
|
|
16
|
+
export interface RouteAwarePaymentMiddlewareConfig {
|
|
17
|
+
route?: string;
|
|
18
|
+
routes?: string[];
|
|
19
|
+
perRoute?: Record<string, PaymentMiddlewareConfigEntry>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PaymentMiddlewareConfigEntry {
|
|
23
|
+
requirements: PaymentRequirements;
|
|
24
|
+
facilitatorUrl?: string;
|
|
25
|
+
network?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AugmentedRequest extends Request {
|
|
29
|
+
payment?: {
|
|
30
|
+
payload: PaymentPayloadV2;
|
|
31
|
+
payerAddress: string;
|
|
32
|
+
verified: boolean;
|
|
33
|
+
route?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ResolvedRouteConfig {
|
|
38
|
+
pattern: string;
|
|
39
|
+
config: PaymentMiddlewareConfigEntry;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const resolveRouteConfig = (
|
|
43
|
+
config: RouteAwarePaymentMiddlewareConfig
|
|
44
|
+
): { routes: ResolvedRouteConfig[]; error?: RouteValidationError } => {
|
|
45
|
+
const validationError = validateRouteConfig(config);
|
|
46
|
+
if (validationError) {
|
|
47
|
+
return { routes: [], error: validationError };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { route, routes, perRoute } = config;
|
|
51
|
+
const routePatterns = route ? [route] : routes || [];
|
|
52
|
+
|
|
53
|
+
const resolvedRoutes: ResolvedRouteConfig[] = [];
|
|
54
|
+
|
|
55
|
+
for (const pattern of routePatterns) {
|
|
56
|
+
const routeConfig = perRoute?.[pattern];
|
|
57
|
+
if (routeConfig) {
|
|
58
|
+
resolvedRoutes.push({
|
|
59
|
+
pattern,
|
|
60
|
+
config: routeConfig,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { routes: resolvedRoutes };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const routeAwarePaymentMiddleware = (
|
|
69
|
+
perRouteConfig: Record<string, PaymentMiddlewareConfigEntry>
|
|
70
|
+
) => {
|
|
71
|
+
const config: RouteAwarePaymentMiddlewareConfig = {
|
|
72
|
+
routes: Object.keys(perRouteConfig),
|
|
73
|
+
perRoute: perRouteConfig,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const { routes: resolvedRoutes, error: configError } = resolveRouteConfig(config);
|
|
77
|
+
|
|
78
|
+
return async (req: AugmentedRequest, res: Response, next: NextFunction): Promise<void> => {
|
|
79
|
+
try {
|
|
80
|
+
if (configError) {
|
|
81
|
+
res.statusCode = 500;
|
|
82
|
+
res.json({
|
|
83
|
+
error: "Payment middleware configuration error",
|
|
84
|
+
details: configError.message,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const path = req.path;
|
|
90
|
+
const matchedRoute = resolvedRoutes.find((r) => matchRoute(r.pattern, path));
|
|
91
|
+
|
|
92
|
+
if (!matchedRoute) {
|
|
93
|
+
next();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const routeConfig = matchedRoute.config;
|
|
98
|
+
const { requirements, facilitatorUrl, network = "base" } = routeConfig;
|
|
99
|
+
|
|
100
|
+
const paymentHeader = req.headers[PAYMENT_SIGNATURE_HEADER.toLowerCase()] as string | undefined;
|
|
101
|
+
|
|
102
|
+
if (!paymentHeader) {
|
|
103
|
+
const requiredHeaders = createPaymentRequiredHeaders(requirements);
|
|
104
|
+
res.statusCode = 402;
|
|
105
|
+
for (const [key, value] of Object.entries(requiredHeaders)) {
|
|
106
|
+
res.setHeader(key, value);
|
|
107
|
+
}
|
|
108
|
+
res.json({
|
|
109
|
+
error: "Payment required",
|
|
110
|
+
accepts: [requirements],
|
|
111
|
+
});
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let paymentPayload: PaymentPayloadV2;
|
|
116
|
+
try {
|
|
117
|
+
paymentPayload = decodePaymentV2(paymentHeader);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
res.statusCode = 400;
|
|
120
|
+
res.json({
|
|
121
|
+
error: "Invalid payment payload",
|
|
122
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const payerAddress = paymentPayload.payload.authorization.from;
|
|
128
|
+
|
|
129
|
+
const settlement = {
|
|
130
|
+
success: true,
|
|
131
|
+
transaction: "",
|
|
132
|
+
network,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const settlementHeaders = createSettlementHeaders(settlement);
|
|
136
|
+
for (const [key, value] of Object.entries(settlementHeaders)) {
|
|
137
|
+
res.setHeader(key, value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
req.payment = {
|
|
141
|
+
payload: paymentPayload,
|
|
142
|
+
payerAddress,
|
|
143
|
+
verified: true,
|
|
144
|
+
route: matchedRoute.pattern,
|
|
145
|
+
};
|
|
146
|
+
next();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
res.statusCode = 500;
|
|
149
|
+
res.json({
|
|
150
|
+
error: "Payment middleware error",
|
|
151
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
};
|