@armory-sh/middleware-express 0.4.6 → 0.4.8

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,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.6",
3
+ "version": "0.4.8",
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.19"
30
+ "@armory-sh/base": "0.2.21"
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, skipVerification = false, network = "base" } = config;
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: !skipVerification };
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";
@@ -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
+ };