@armory-sh/middleware-hono 0.3.18 → 0.3.20
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 +220 -0
- package/dist/extensions.d.ts +39 -0
- package/dist/extensions.js +60 -0
- package/dist/index.d.ts +42 -5
- package/dist/index.js +392 -17
- package/package.json +9 -2
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @armory-sh/middleware-hono
|
|
2
|
+
|
|
3
|
+
x402 payment middleware for Hono applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @armory-sh/middleware-hono
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Simple payment middleware for Hono
|
|
14
|
+
- Route-aware payment configuration
|
|
15
|
+
- Multi-network, multi-token support
|
|
16
|
+
- Per-route facilitator configuration
|
|
17
|
+
- Full TypeScript support
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Hono } from "hono";
|
|
23
|
+
import { paymentMiddleware } from "@armory-sh/middleware-hono";
|
|
24
|
+
|
|
25
|
+
const app = new Hono();
|
|
26
|
+
|
|
27
|
+
app.use("/*", paymentMiddleware({
|
|
28
|
+
payTo: "0xYourAddress...",
|
|
29
|
+
chain: "base",
|
|
30
|
+
token: "usdc",
|
|
31
|
+
amount: "1.0",
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
app.get("/api/data", (c) => {
|
|
35
|
+
const payment = c.get("payment");
|
|
36
|
+
return c.json({
|
|
37
|
+
data: "protected data",
|
|
38
|
+
payerAddress: payment?.payload?.authorization?.from,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Route-Aware Middleware
|
|
44
|
+
|
|
45
|
+
Configure different payment requirements for different routes:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { routeAwarePaymentMiddleware } from "@armory-sh/middleware-hono";
|
|
49
|
+
|
|
50
|
+
const app = new Hono();
|
|
51
|
+
|
|
52
|
+
// Single route with wildcard
|
|
53
|
+
app.use("/api/premium/*", routeAwarePaymentMiddleware({
|
|
54
|
+
routes: ["/api/premium/*"],
|
|
55
|
+
payTo: "0xYourAddress...",
|
|
56
|
+
amount: "$5.00",
|
|
57
|
+
network: "base"
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
// Multiple routes with per-route configuration
|
|
61
|
+
app.use("/api/*", routeAwarePaymentMiddleware({
|
|
62
|
+
routes: ["/api/basic", "/api/premium/*"],
|
|
63
|
+
payTo: "0xYourAddress...",
|
|
64
|
+
amount: "$1.00", // Default amount
|
|
65
|
+
network: "base",
|
|
66
|
+
perRoute: {
|
|
67
|
+
"/api/premium/*": {
|
|
68
|
+
amount: "$5.00", // Override for premium routes
|
|
69
|
+
network: "ethereum",
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Configuration Options
|
|
76
|
+
|
|
77
|
+
### PaymentConfig
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface PaymentConfig {
|
|
81
|
+
payTo: string; // Payment recipient address
|
|
82
|
+
chain?: string | number; // Network (name or chain ID)
|
|
83
|
+
chains?: Array<string | number>; // Multiple networks
|
|
84
|
+
token?: string; // Token symbol
|
|
85
|
+
tokens?: string[]; // Multiple tokens
|
|
86
|
+
amount?: string; // Amount (default: "1.0")
|
|
87
|
+
maxTimeoutSeconds?: number; // Payment timeout (default: 300)
|
|
88
|
+
|
|
89
|
+
// Per-chain configuration
|
|
90
|
+
payToByChain?: Record<string, string>;
|
|
91
|
+
facilitatorUrlByChain?: Record<string, string>;
|
|
92
|
+
|
|
93
|
+
// Per-token-per-chain configuration
|
|
94
|
+
payToByToken?: Record<string, Record<string, string>>;
|
|
95
|
+
facilitatorUrlByToken?: Record<string, Record<string, string>>;
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### RouteAwarePaymentConfig
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface RouteAwarePaymentConfig extends PaymentConfig {
|
|
103
|
+
route?: string; // Single exact route (no wildcards)
|
|
104
|
+
routes?: string[]; // Multiple routes (allows wildcards)
|
|
105
|
+
perRoute?: Record<string, Partial<PaymentConfig>>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Payment Context
|
|
110
|
+
|
|
111
|
+
The middleware adds payment information to the Hono context:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
app.get("/api/data", (c) => {
|
|
115
|
+
const payment = c.get("payment");
|
|
116
|
+
// payment.payload: PaymentPayloadV2
|
|
117
|
+
// payment.verified: boolean
|
|
118
|
+
// payment.route: string (only for route-aware middleware)
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Input Formats
|
|
123
|
+
|
|
124
|
+
**Networks** - Use any format:
|
|
125
|
+
```typescript
|
|
126
|
+
'base' // name
|
|
127
|
+
8453 // chain ID
|
|
128
|
+
'eip155:8453' // CAIP-2
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Tokens** - Use any format:
|
|
132
|
+
```typescript
|
|
133
|
+
'usdc' // symbol (case-insensitive)
|
|
134
|
+
'0x8335...' // EVM address
|
|
135
|
+
'eip155:8453/erc20:0x8335...' // CAIP Asset ID
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Route Pattern Matching
|
|
139
|
+
|
|
140
|
+
- **Exact**: `/api/users` - matches only `/api/users`
|
|
141
|
+
- **Wildcard**: `/api/*` - matches `/api/users`, `/api/posts/123`
|
|
142
|
+
- **Parameterized**: `/api/users/:id` - matches `/api/users/123`
|
|
143
|
+
|
|
144
|
+
Priority order: Exact matches > Parameterized routes > Wildcard routes
|
|
145
|
+
|
|
146
|
+
## Examples
|
|
147
|
+
|
|
148
|
+
### Multi-Network Support
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
app.use("/*", paymentMiddleware({
|
|
152
|
+
payTo: "0xYourAddress...",
|
|
153
|
+
chains: ["base", "ethereum", "skale-base"],
|
|
154
|
+
tokens: ["usdc", "eurc"],
|
|
155
|
+
amount: "1.0"
|
|
156
|
+
}));
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Per-Chain Configuration
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
app.use("/*", paymentMiddleware({
|
|
163
|
+
payTo: "0xDefaultAddress...",
|
|
164
|
+
payToByChain: {
|
|
165
|
+
base: "0xBaseAddress...",
|
|
166
|
+
ethereum: "0xEthAddress...",
|
|
167
|
+
},
|
|
168
|
+
chain: "base",
|
|
169
|
+
token: "usdc"
|
|
170
|
+
}));
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Route-Specific Pricing
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
app.use("/api/*", routeAwarePaymentMiddleware({
|
|
177
|
+
routes: ["/api/basic", "/api/pro", "/api/enterprise"],
|
|
178
|
+
payTo: "0xYourAddress...",
|
|
179
|
+
amount: "$1.00",
|
|
180
|
+
network: "base",
|
|
181
|
+
perRoute: {
|
|
182
|
+
"/api/pro": {
|
|
183
|
+
amount: "$5.00",
|
|
184
|
+
},
|
|
185
|
+
"/api/enterprise": {
|
|
186
|
+
amount: "$50.00",
|
|
187
|
+
network: "ethereum",
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}));
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## API
|
|
194
|
+
|
|
195
|
+
### `paymentMiddleware(config)`
|
|
196
|
+
|
|
197
|
+
Creates a basic payment middleware for Hono.
|
|
198
|
+
|
|
199
|
+
**Parameters:**
|
|
200
|
+
- `config`: Payment configuration
|
|
201
|
+
|
|
202
|
+
**Returns:** Hono middleware function
|
|
203
|
+
|
|
204
|
+
### `createPaymentRequirements(config)`
|
|
205
|
+
|
|
206
|
+
Creates payment requirements from configuration (for advanced use).
|
|
207
|
+
|
|
208
|
+
**Parameters:**
|
|
209
|
+
- `config`: Payment configuration
|
|
210
|
+
|
|
211
|
+
**Returns:** Object with `requirements` array and optional `error`
|
|
212
|
+
|
|
213
|
+
### `routeAwarePaymentMiddleware(config)`
|
|
214
|
+
|
|
215
|
+
Creates a route-aware payment middleware for Hono.
|
|
216
|
+
|
|
217
|
+
**Parameters:**
|
|
218
|
+
- `config`: Route-aware payment configuration
|
|
219
|
+
|
|
220
|
+
**Returns:** Hono middleware function
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Extensions } from '@armory-sh/base';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extension handling for Hono middleware
|
|
5
|
+
* Integrates with @armory-sh/extensions package
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type BazaarDiscoveryConfig = {
|
|
9
|
+
input?: unknown;
|
|
10
|
+
inputSchema?: Record<string, unknown>;
|
|
11
|
+
bodyType?: "json" | "form-data" | "text";
|
|
12
|
+
output?: {
|
|
13
|
+
example?: unknown;
|
|
14
|
+
schema?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
type SIWxExtensionConfig = {
|
|
18
|
+
domain?: string;
|
|
19
|
+
resourceUri?: string;
|
|
20
|
+
network?: string | string[];
|
|
21
|
+
statement?: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
expirationSeconds?: number;
|
|
24
|
+
};
|
|
25
|
+
type Extension<T = unknown> = {
|
|
26
|
+
info: T;
|
|
27
|
+
schema: Record<string, unknown>;
|
|
28
|
+
};
|
|
29
|
+
interface ExtensionConfig {
|
|
30
|
+
bazaar?: BazaarDiscoveryConfig;
|
|
31
|
+
signInWithX?: SIWxExtensionConfig;
|
|
32
|
+
}
|
|
33
|
+
interface PaymentConfigWithExtensions {
|
|
34
|
+
extensions?: ExtensionConfig;
|
|
35
|
+
}
|
|
36
|
+
declare function buildExtensions(config: ExtensionConfig): Extensions;
|
|
37
|
+
declare function extractExtension<T>(extensions: Extensions | undefined, key: string): Extension<T> | null;
|
|
38
|
+
|
|
39
|
+
export { type ExtensionConfig, type PaymentConfigWithExtensions, buildExtensions, extractExtension };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/extensions.ts
|
|
2
|
+
var declareDiscoveryExtension = (config = {}) => {
|
|
3
|
+
const info = {};
|
|
4
|
+
if (config.input !== void 0) {
|
|
5
|
+
info.input = config.input;
|
|
6
|
+
}
|
|
7
|
+
if (config.inputSchema !== void 0) {
|
|
8
|
+
info.inputSchema = config.inputSchema;
|
|
9
|
+
}
|
|
10
|
+
if (config.output !== void 0) {
|
|
11
|
+
info.output = config.output;
|
|
12
|
+
}
|
|
13
|
+
return { info, schema: { type: "object" } };
|
|
14
|
+
};
|
|
15
|
+
var declareSIWxExtension = (config = {}) => {
|
|
16
|
+
const info = {};
|
|
17
|
+
if (config.domain !== void 0) {
|
|
18
|
+
info.domain = config.domain;
|
|
19
|
+
}
|
|
20
|
+
if (config.resourceUri !== void 0) {
|
|
21
|
+
info.resourceUri = config.resourceUri;
|
|
22
|
+
}
|
|
23
|
+
if (config.network !== void 0) {
|
|
24
|
+
info.network = config.network;
|
|
25
|
+
}
|
|
26
|
+
if (config.statement !== void 0) {
|
|
27
|
+
info.statement = config.statement;
|
|
28
|
+
}
|
|
29
|
+
if (config.version !== void 0) {
|
|
30
|
+
info.version = config.version;
|
|
31
|
+
}
|
|
32
|
+
if (config.expirationSeconds !== void 0) {
|
|
33
|
+
info.expirationSeconds = config.expirationSeconds;
|
|
34
|
+
}
|
|
35
|
+
return { info, schema: { type: "object" } };
|
|
36
|
+
};
|
|
37
|
+
function buildExtensions(config) {
|
|
38
|
+
const extensions = {};
|
|
39
|
+
if (config.bazaar) {
|
|
40
|
+
extensions.bazaar = declareDiscoveryExtension(config.bazaar);
|
|
41
|
+
}
|
|
42
|
+
if (config.signInWithX) {
|
|
43
|
+
extensions["sign-in-with-x"] = declareSIWxExtension(config.signInWithX);
|
|
44
|
+
}
|
|
45
|
+
return extensions;
|
|
46
|
+
}
|
|
47
|
+
function extractExtension(extensions, key) {
|
|
48
|
+
if (!extensions || typeof extensions !== "object") {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const extension = extensions[key];
|
|
52
|
+
if (!extension || typeof extension !== "object") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return extension;
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
buildExtensions,
|
|
59
|
+
extractExtension
|
|
60
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
import { Context, Next } from 'hono';
|
|
2
|
-
import {
|
|
2
|
+
import { PaymentRequirementsV2, ValidationError, PaymentRequirements, PaymentPayloadV2 } from '@armory-sh/base';
|
|
3
|
+
import { ExtensionConfig } from './extensions.js';
|
|
4
|
+
export { buildExtensions, extractExtension } from './extensions.js';
|
|
3
5
|
|
|
4
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Simplified middleware API for easy x402 payment integration
|
|
8
|
+
* Just specify payTo address and optional chains/tokens by name
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
type NetworkId = string | number;
|
|
12
|
+
type TokenId = string;
|
|
13
|
+
interface PaymentConfig {
|
|
14
|
+
payTo: string;
|
|
15
|
+
chains?: NetworkId[];
|
|
16
|
+
chain?: NetworkId;
|
|
17
|
+
tokens?: TokenId[];
|
|
18
|
+
token?: TokenId;
|
|
19
|
+
amount?: string;
|
|
20
|
+
maxTimeoutSeconds?: number;
|
|
21
|
+
payToByChain?: Record<NetworkId, string>;
|
|
22
|
+
payToByToken?: Record<NetworkId, Record<TokenId, string>>;
|
|
23
|
+
facilitatorUrl?: string;
|
|
24
|
+
facilitatorUrlByChain?: Record<NetworkId, string>;
|
|
25
|
+
facilitatorUrlByToken?: Record<NetworkId, Record<TokenId, string>>;
|
|
26
|
+
extensions?: ExtensionConfig;
|
|
27
|
+
}
|
|
28
|
+
interface ResolvedSimpleConfig {
|
|
29
|
+
requirements: PaymentRequirementsV2[];
|
|
30
|
+
error?: ValidationError;
|
|
31
|
+
}
|
|
32
|
+
declare function createPaymentRequirements(config: PaymentConfig): ResolvedSimpleConfig;
|
|
33
|
+
declare function paymentMiddleware(config: PaymentConfig): (c: Context, next: Next) => Promise<Response | void>;
|
|
34
|
+
|
|
35
|
+
interface RouteAwarePaymentConfig extends PaymentConfig {
|
|
36
|
+
route?: string;
|
|
37
|
+
routes?: string[];
|
|
38
|
+
perRoute?: Record<string, Partial<PaymentConfig>>;
|
|
39
|
+
}
|
|
40
|
+
declare const routeAwarePaymentMiddleware: (config: RouteAwarePaymentConfig) => (c: Context, next: Next) => Promise<Response | void>;
|
|
41
|
+
|
|
42
|
+
interface AdvancedPaymentConfig {
|
|
5
43
|
requirements: PaymentRequirements;
|
|
6
44
|
facilitatorUrl?: string;
|
|
7
|
-
skipVerification?: boolean;
|
|
8
45
|
network?: string;
|
|
9
46
|
}
|
|
10
47
|
interface AugmentedRequest extends Request {
|
|
@@ -14,6 +51,6 @@ interface AugmentedRequest extends Request {
|
|
|
14
51
|
verified: boolean;
|
|
15
52
|
};
|
|
16
53
|
}
|
|
17
|
-
declare const
|
|
54
|
+
declare const advancedPaymentMiddleware: (config: AdvancedPaymentConfig) => (c: Context, next: Next) => Promise<Response | void>;
|
|
18
55
|
|
|
19
|
-
export { type AugmentedRequest, type
|
|
56
|
+
export { type AdvancedPaymentConfig, type AugmentedRequest, ExtensionConfig, type PaymentConfig, type RouteAwarePaymentConfig, advancedPaymentMiddleware, createPaymentRequirements, paymentMiddleware, routeAwarePaymentMiddleware };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,394 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
3
|
createPaymentRequiredHeaders,
|
|
4
|
-
createSettlementHeaders,
|
|
5
4
|
PAYMENT_SIGNATURE_HEADER,
|
|
6
5
|
decodePaymentV2
|
|
7
6
|
} from "@armory-sh/base";
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
|
|
8
|
+
// src/simple.ts
|
|
9
|
+
import {
|
|
10
|
+
resolveNetwork,
|
|
11
|
+
resolveToken,
|
|
12
|
+
safeBase64Encode,
|
|
13
|
+
V2_HEADERS,
|
|
14
|
+
registerToken
|
|
15
|
+
} from "@armory-sh/base";
|
|
16
|
+
import {
|
|
17
|
+
TOKENS
|
|
18
|
+
} from "@armory-sh/base";
|
|
19
|
+
|
|
20
|
+
// src/extensions.ts
|
|
21
|
+
var declareDiscoveryExtension = (config = {}) => {
|
|
22
|
+
const info = {};
|
|
23
|
+
if (config.input !== void 0) {
|
|
24
|
+
info.input = config.input;
|
|
25
|
+
}
|
|
26
|
+
if (config.inputSchema !== void 0) {
|
|
27
|
+
info.inputSchema = config.inputSchema;
|
|
28
|
+
}
|
|
29
|
+
if (config.output !== void 0) {
|
|
30
|
+
info.output = config.output;
|
|
31
|
+
}
|
|
32
|
+
return { info, schema: { type: "object" } };
|
|
33
|
+
};
|
|
34
|
+
var declareSIWxExtension = (config = {}) => {
|
|
35
|
+
const info = {};
|
|
36
|
+
if (config.domain !== void 0) {
|
|
37
|
+
info.domain = config.domain;
|
|
38
|
+
}
|
|
39
|
+
if (config.resourceUri !== void 0) {
|
|
40
|
+
info.resourceUri = config.resourceUri;
|
|
41
|
+
}
|
|
42
|
+
if (config.network !== void 0) {
|
|
43
|
+
info.network = config.network;
|
|
44
|
+
}
|
|
45
|
+
if (config.statement !== void 0) {
|
|
46
|
+
info.statement = config.statement;
|
|
47
|
+
}
|
|
48
|
+
if (config.version !== void 0) {
|
|
49
|
+
info.version = config.version;
|
|
50
|
+
}
|
|
51
|
+
if (config.expirationSeconds !== void 0) {
|
|
52
|
+
info.expirationSeconds = config.expirationSeconds;
|
|
53
|
+
}
|
|
54
|
+
return { info, schema: { type: "object" } };
|
|
55
|
+
};
|
|
56
|
+
function buildExtensions(config) {
|
|
57
|
+
const extensions = {};
|
|
58
|
+
if (config.bazaar) {
|
|
59
|
+
extensions.bazaar = declareDiscoveryExtension(config.bazaar);
|
|
60
|
+
}
|
|
61
|
+
if (config.signInWithX) {
|
|
62
|
+
extensions["sign-in-with-x"] = declareSIWxExtension(config.signInWithX);
|
|
63
|
+
}
|
|
64
|
+
return extensions;
|
|
65
|
+
}
|
|
66
|
+
function extractExtension(extensions, key) {
|
|
67
|
+
if (!extensions || typeof extensions !== "object") {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const extension = extensions[key];
|
|
71
|
+
if (!extension || typeof extension !== "object") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
return extension;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/simple.ts
|
|
78
|
+
var isValidationError = (value) => {
|
|
79
|
+
return typeof value === "object" && value !== null && "code" in value;
|
|
80
|
+
};
|
|
81
|
+
function ensureTokensRegistered() {
|
|
82
|
+
for (const token of Object.values(TOKENS)) {
|
|
83
|
+
try {
|
|
84
|
+
registerToken(token);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function resolveChainTokenInputs(chains, tokens) {
|
|
90
|
+
const resolvedNetworks = [];
|
|
91
|
+
const resolvedTokens = [];
|
|
92
|
+
const errors = [];
|
|
93
|
+
const networkInputs = chains?.length ? chains : Object.keys({
|
|
94
|
+
ethereum: 1,
|
|
95
|
+
base: 8453,
|
|
96
|
+
"base-sepolia": 84532,
|
|
97
|
+
"skale-base": 1187947933,
|
|
98
|
+
"skale-base-sepolia": 324705682,
|
|
99
|
+
"ethereum-sepolia": 11155111
|
|
100
|
+
});
|
|
101
|
+
for (const networkId of networkInputs) {
|
|
102
|
+
const resolved = resolveNetwork(networkId);
|
|
103
|
+
if (isValidationError(resolved)) {
|
|
104
|
+
errors.push(`Network "${networkId}": ${resolved.message}`);
|
|
105
|
+
} else {
|
|
106
|
+
resolvedNetworks.push(resolved);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const tokenInputs = tokens?.length ? tokens : ["usdc"];
|
|
110
|
+
for (const tokenId of tokenInputs) {
|
|
111
|
+
let found = false;
|
|
112
|
+
for (const network of resolvedNetworks) {
|
|
113
|
+
const resolved = resolveToken(tokenId, network);
|
|
114
|
+
if (isValidationError(resolved)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
resolvedTokens.push(resolved);
|
|
118
|
+
found = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
if (!found) {
|
|
122
|
+
errors.push(`Token "${tokenId}" not found on any specified network`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (errors.length > 0) {
|
|
126
|
+
return {
|
|
127
|
+
networks: resolvedNetworks,
|
|
128
|
+
tokens: resolvedTokens,
|
|
129
|
+
error: {
|
|
130
|
+
code: "VALIDATION_FAILED",
|
|
131
|
+
message: errors.join("; ")
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
return { networks: resolvedNetworks, tokens: resolvedTokens };
|
|
136
|
+
}
|
|
137
|
+
function toAtomicUnits(amount) {
|
|
138
|
+
if (amount.includes(".")) {
|
|
139
|
+
const [whole, fractional = ""] = amount.split(".");
|
|
140
|
+
const paddedFractional = fractional.padEnd(6, "0").slice(0, 6);
|
|
141
|
+
return `${whole}${paddedFractional}`.replace(/^0+/, "") || "0";
|
|
142
|
+
}
|
|
143
|
+
return `${amount}000000`;
|
|
144
|
+
}
|
|
145
|
+
function resolvePayTo(config, network, token) {
|
|
146
|
+
const chainId = network.config.chainId;
|
|
147
|
+
if (config.payToByToken) {
|
|
148
|
+
for (const [chainKey, tokenMap] of Object.entries(config.payToByToken)) {
|
|
149
|
+
const resolvedChain = resolveNetwork(chainKey);
|
|
150
|
+
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
151
|
+
for (const [tokenKey, address] of Object.entries(tokenMap)) {
|
|
152
|
+
const resolvedToken = resolveToken(tokenKey, network);
|
|
153
|
+
if (!isValidationError(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === token.config.contractAddress.toLowerCase()) {
|
|
154
|
+
return address;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (config.payToByChain) {
|
|
161
|
+
for (const [chainKey, address] of Object.entries(config.payToByChain)) {
|
|
162
|
+
const resolvedChain = resolveNetwork(chainKey);
|
|
163
|
+
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
164
|
+
return address;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return config.payTo;
|
|
169
|
+
}
|
|
170
|
+
function resolveFacilitatorUrl(config, network, token) {
|
|
171
|
+
const chainId = network.config.chainId;
|
|
172
|
+
if (config.facilitatorUrlByToken) {
|
|
173
|
+
for (const [chainKey, tokenMap] of Object.entries(config.facilitatorUrlByToken)) {
|
|
174
|
+
const resolvedChain = resolveNetwork(chainKey);
|
|
175
|
+
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
176
|
+
for (const [tokenKey, url] of Object.entries(tokenMap)) {
|
|
177
|
+
const resolvedToken = resolveToken(tokenKey, network);
|
|
178
|
+
if (!isValidationError(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === token.config.contractAddress.toLowerCase()) {
|
|
179
|
+
return url;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (config.facilitatorUrlByChain) {
|
|
186
|
+
for (const [chainKey, url] of Object.entries(config.facilitatorUrlByChain)) {
|
|
187
|
+
const resolvedChain = resolveNetwork(chainKey);
|
|
188
|
+
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
189
|
+
return url;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return config.facilitatorUrl;
|
|
194
|
+
}
|
|
195
|
+
function createPaymentRequirements(config) {
|
|
196
|
+
ensureTokensRegistered();
|
|
197
|
+
const {
|
|
198
|
+
payTo,
|
|
199
|
+
chains,
|
|
200
|
+
chain,
|
|
201
|
+
tokens,
|
|
202
|
+
token,
|
|
203
|
+
amount = "1.0",
|
|
204
|
+
maxTimeoutSeconds = 300
|
|
205
|
+
} = config;
|
|
206
|
+
const chainInputs = chain ? [chain] : chains;
|
|
207
|
+
const tokenInputs = token ? [token] : tokens;
|
|
208
|
+
const { networks, tokens: resolvedTokens, error } = resolveChainTokenInputs(
|
|
209
|
+
chainInputs,
|
|
210
|
+
tokenInputs
|
|
211
|
+
);
|
|
212
|
+
if (error) {
|
|
213
|
+
return { requirements: [], error };
|
|
214
|
+
}
|
|
215
|
+
const requirements = [];
|
|
216
|
+
for (const network of networks) {
|
|
217
|
+
for (const resolvedToken of resolvedTokens) {
|
|
218
|
+
if (resolvedToken.network.config.chainId !== network.config.chainId) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const atomicAmount = toAtomicUnits(amount);
|
|
222
|
+
const tokenConfig = resolvedToken.config;
|
|
223
|
+
const resolvedPayTo = resolvePayTo(config, network, resolvedToken);
|
|
224
|
+
const resolvedFacilitatorUrl = resolveFacilitatorUrl(config, network, resolvedToken);
|
|
225
|
+
requirements.push({
|
|
226
|
+
scheme: "exact",
|
|
227
|
+
network: network.caip2,
|
|
228
|
+
amount: atomicAmount,
|
|
229
|
+
asset: tokenConfig.contractAddress,
|
|
230
|
+
payTo: resolvedPayTo,
|
|
231
|
+
maxTimeoutSeconds,
|
|
232
|
+
extra: {
|
|
233
|
+
name: tokenConfig.name,
|
|
234
|
+
version: tokenConfig.version,
|
|
235
|
+
...resolvedFacilitatorUrl && { facilitatorUrl: resolvedFacilitatorUrl }
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { requirements };
|
|
241
|
+
}
|
|
242
|
+
function paymentMiddleware(config) {
|
|
243
|
+
const { requirements, error } = createPaymentRequirements(config);
|
|
244
|
+
return async (c, next) => {
|
|
245
|
+
if (error) {
|
|
246
|
+
c.status(500);
|
|
247
|
+
return c.json({
|
|
248
|
+
error: "Payment middleware configuration error",
|
|
249
|
+
details: error.message
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const paymentHeader = c.req.header(V2_HEADERS.PAYMENT_SIGNATURE);
|
|
253
|
+
if (!paymentHeader) {
|
|
254
|
+
const resource = {
|
|
255
|
+
url: c.req.url,
|
|
256
|
+
description: "API Access",
|
|
257
|
+
mimeType: "application/json"
|
|
258
|
+
};
|
|
259
|
+
const paymentRequired = {
|
|
260
|
+
x402Version: 2,
|
|
261
|
+
error: "Payment required",
|
|
262
|
+
resource,
|
|
263
|
+
accepts: requirements,
|
|
264
|
+
extensions: config.extensions ? buildExtensions(config.extensions) : void 0
|
|
265
|
+
};
|
|
266
|
+
c.status(402);
|
|
267
|
+
c.header(V2_HEADERS.PAYMENT_REQUIRED, safeBase64Encode(JSON.stringify(paymentRequired)));
|
|
268
|
+
return c.json({
|
|
269
|
+
error: "Payment required",
|
|
270
|
+
accepts: requirements
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const payload = paymentHeader;
|
|
274
|
+
let parsedPayload;
|
|
275
|
+
try {
|
|
276
|
+
const decoded = atob(payload);
|
|
277
|
+
parsedPayload = JSON.parse(decoded);
|
|
278
|
+
} catch {
|
|
279
|
+
try {
|
|
280
|
+
parsedPayload = JSON.parse(payload);
|
|
281
|
+
} catch {
|
|
282
|
+
c.status(400);
|
|
283
|
+
return c.json({ error: "Invalid payment payload" });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
c.set("payment", {
|
|
287
|
+
payload: parsedPayload,
|
|
288
|
+
verified: false
|
|
289
|
+
});
|
|
290
|
+
return next();
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/routes.ts
|
|
295
|
+
import {
|
|
296
|
+
safeBase64Encode as safeBase64Encode2,
|
|
297
|
+
V2_HEADERS as V2_HEADERS2,
|
|
298
|
+
matchRoute,
|
|
299
|
+
validateRouteConfig
|
|
300
|
+
} from "@armory-sh/base";
|
|
301
|
+
var resolveRouteConfig = (config) => {
|
|
302
|
+
const validationError = validateRouteConfig(config);
|
|
303
|
+
if (validationError) {
|
|
304
|
+
return { routes: [], error: validationError };
|
|
305
|
+
}
|
|
306
|
+
const { route, routes, perRoute, ...baseConfig } = config;
|
|
307
|
+
const routePatterns = route ? [route] : routes || [];
|
|
308
|
+
const resolvedRoutes = [];
|
|
309
|
+
for (const pattern of routePatterns) {
|
|
310
|
+
const perRouteOverride = perRoute?.[pattern];
|
|
311
|
+
const mergedConfig = {
|
|
312
|
+
...baseConfig,
|
|
313
|
+
...perRouteOverride
|
|
314
|
+
};
|
|
315
|
+
resolvedRoutes.push({
|
|
316
|
+
pattern,
|
|
317
|
+
config: mergedConfig
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return { routes: resolvedRoutes };
|
|
321
|
+
};
|
|
322
|
+
var routeAwarePaymentMiddleware = (config) => {
|
|
323
|
+
const { routes, error } = resolveRouteConfig(config);
|
|
324
|
+
return async (c, next) => {
|
|
325
|
+
if (error) {
|
|
326
|
+
c.status(500);
|
|
327
|
+
return c.json({
|
|
328
|
+
error: "Payment middleware configuration error",
|
|
329
|
+
details: error.message
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const path = new URL(c.req.url).pathname;
|
|
333
|
+
const matchedRoute = routes.find((r) => matchRoute(r.pattern, path));
|
|
334
|
+
if (!matchedRoute) {
|
|
335
|
+
return next();
|
|
336
|
+
}
|
|
337
|
+
const { requirements, error: requirementsError } = createPaymentRequirements(matchedRoute.config);
|
|
338
|
+
if (requirementsError) {
|
|
339
|
+
c.status(500);
|
|
340
|
+
return c.json({
|
|
341
|
+
error: "Payment middleware configuration error",
|
|
342
|
+
details: requirementsError.message
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const paymentHeader = c.req.header(V2_HEADERS2.PAYMENT_SIGNATURE);
|
|
346
|
+
if (!paymentHeader) {
|
|
347
|
+
const resource = {
|
|
348
|
+
url: c.req.url,
|
|
349
|
+
description: "API Access",
|
|
350
|
+
mimeType: "application/json"
|
|
351
|
+
};
|
|
352
|
+
const paymentRequired = {
|
|
353
|
+
x402Version: 2,
|
|
354
|
+
error: "Payment required",
|
|
355
|
+
resource,
|
|
356
|
+
accepts: requirements
|
|
357
|
+
};
|
|
358
|
+
c.status(402);
|
|
359
|
+
c.header(
|
|
360
|
+
V2_HEADERS2.PAYMENT_REQUIRED,
|
|
361
|
+
safeBase64Encode2(JSON.stringify(paymentRequired))
|
|
362
|
+
);
|
|
363
|
+
return c.json({
|
|
364
|
+
error: "Payment required",
|
|
365
|
+
accepts: requirements
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
let parsedPayload;
|
|
369
|
+
try {
|
|
370
|
+
const decoded = atob(paymentHeader);
|
|
371
|
+
parsedPayload = JSON.parse(decoded);
|
|
372
|
+
} catch {
|
|
373
|
+
try {
|
|
374
|
+
parsedPayload = JSON.parse(paymentHeader);
|
|
375
|
+
} catch {
|
|
376
|
+
c.status(400);
|
|
377
|
+
return c.json({ error: "Invalid payment payload" });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
c.set("payment", {
|
|
381
|
+
payload: parsedPayload,
|
|
382
|
+
verified: false,
|
|
383
|
+
route: matchedRoute.pattern
|
|
384
|
+
});
|
|
385
|
+
return next();
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// src/index.ts
|
|
390
|
+
var advancedPaymentMiddleware = (config) => {
|
|
391
|
+
const { requirements, facilitatorUrl, network = "base" } = config;
|
|
10
392
|
return async (c, next) => {
|
|
11
393
|
const paymentHeader = c.req.header(PAYMENT_SIGNATURE_HEADER);
|
|
12
394
|
if (!paymentHeader) {
|
|
@@ -22,11 +404,8 @@ var paymentMiddleware = (config) => {
|
|
|
22
404
|
}
|
|
23
405
|
let paymentPayload = null;
|
|
24
406
|
try {
|
|
25
|
-
console.log("[x402 Debug] Raw payment header:", paymentHeader);
|
|
26
407
|
paymentPayload = decodePaymentV2(paymentHeader);
|
|
27
|
-
console.log("[x402 Debug] Decoded payload:", JSON.stringify(paymentPayload, null, 2));
|
|
28
408
|
} catch (e) {
|
|
29
|
-
console.error("[x402 Debug] Decode error:", e);
|
|
30
409
|
c.status(400);
|
|
31
410
|
return c.json({ error: "Invalid payment payload", details: String(e) });
|
|
32
411
|
}
|
|
@@ -35,23 +414,19 @@ var paymentMiddleware = (config) => {
|
|
|
35
414
|
return c.json({ error: "Invalid payment payload" });
|
|
36
415
|
}
|
|
37
416
|
const payerAddress = paymentPayload.payload.authorization.from;
|
|
38
|
-
const settlement = {
|
|
39
|
-
success: true,
|
|
40
|
-
transaction: "",
|
|
41
|
-
network
|
|
42
|
-
};
|
|
43
|
-
const settlementHeaders = createSettlementHeaders(settlement);
|
|
44
|
-
for (const [key, value] of Object.entries(settlementHeaders)) {
|
|
45
|
-
c.header(key, value);
|
|
46
|
-
}
|
|
47
417
|
c.set("payment", {
|
|
48
418
|
payload: paymentPayload,
|
|
49
419
|
payerAddress,
|
|
50
|
-
verified:
|
|
420
|
+
verified: true
|
|
51
421
|
});
|
|
52
422
|
return next();
|
|
53
423
|
};
|
|
54
424
|
};
|
|
55
425
|
export {
|
|
56
|
-
|
|
426
|
+
advancedPaymentMiddleware,
|
|
427
|
+
buildExtensions,
|
|
428
|
+
createPaymentRequirements,
|
|
429
|
+
extractExtension,
|
|
430
|
+
paymentMiddleware,
|
|
431
|
+
routeAwarePaymentMiddleware
|
|
57
432
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@armory-sh/middleware-hono",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
12
12
|
"bun": "./src/index.ts",
|
|
13
13
|
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./extensions": {
|
|
16
|
+
"types": "./dist/extensions.d.ts",
|
|
17
|
+
"bun": "./src/extensions.ts",
|
|
18
|
+
"default": "./dist/extensions.js"
|
|
14
19
|
}
|
|
15
20
|
},
|
|
16
21
|
"files": [
|
|
@@ -28,10 +33,12 @@
|
|
|
28
33
|
"hono": "^4"
|
|
29
34
|
},
|
|
30
35
|
"dependencies": {
|
|
31
|
-
"@armory-sh/base": "0.2.
|
|
36
|
+
"@armory-sh/base": "0.2.20",
|
|
37
|
+
"@armory-sh/extensions": "0.1.1"
|
|
32
38
|
},
|
|
33
39
|
"devDependencies": {
|
|
34
40
|
"bun-types": "latest",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
35
42
|
"typescript": "5.9.3",
|
|
36
43
|
"hono": "^4"
|
|
37
44
|
},
|