@armory-sh/middleware-hono 0.3.28 → 0.3.30
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 +30 -6
- package/dist/index.d.ts +23 -33
- package/dist/index.js +236 -325
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -14,24 +14,47 @@ bun add @armory-sh/middleware-hono
|
|
|
14
14
|
|
|
15
15
|
Armory enables HTTP API payments via EIP-3009 `transferWithAuthorization`. Accept payments from any x402-compatible client—Coinbase SDK, Armory SDK, or your own implementation.
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## API Reference
|
|
18
|
+
|
|
19
|
+
### Middleware
|
|
18
20
|
|
|
19
21
|
```typescript
|
|
20
22
|
import {
|
|
21
|
-
// Middleware
|
|
22
23
|
paymentMiddleware,
|
|
23
24
|
routeAwarePaymentMiddleware,
|
|
24
25
|
|
|
25
|
-
// Requirements
|
|
26
|
-
createPaymentRequirements,
|
|
27
|
-
|
|
28
26
|
// Types
|
|
29
27
|
type PaymentConfig,
|
|
30
28
|
type RouteAwarePaymentConfig,
|
|
31
|
-
type
|
|
29
|
+
type AugmentedContext,
|
|
32
30
|
} from '@armory-sh/middleware-hono';
|
|
33
31
|
```
|
|
34
32
|
|
|
33
|
+
### Payment Requirements
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import {
|
|
37
|
+
createPaymentRequirements,
|
|
38
|
+
|
|
39
|
+
// Types
|
|
40
|
+
type ResolvedRequirementsConfig,
|
|
41
|
+
} from '@armory-sh/middleware-hono';
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Extensions
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import {
|
|
48
|
+
buildExtensions,
|
|
49
|
+
extractExtension,
|
|
50
|
+
|
|
51
|
+
// Types
|
|
52
|
+
type ExtensionConfig,
|
|
53
|
+
} from '@armory-sh/middleware-hono';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
35
58
|
## Quick Start
|
|
36
59
|
|
|
37
60
|
### Basic Middleware
|
|
@@ -168,6 +191,7 @@ interface RouteAwarePaymentConfig extends PaymentConfig {
|
|
|
168
191
|
| Base Sepolia | 84532 |
|
|
169
192
|
| SKALE Base | 1187947933 |
|
|
170
193
|
| SKALE Base Sepolia | 324705682 |
|
|
194
|
+
| Ethereum Sepolia | 11155111 |
|
|
171
195
|
|
|
172
196
|
## License
|
|
173
197
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,56 +1,46 @@
|
|
|
1
|
+
import { X402PaymentPayload, PaymentRequirementsV2 } from '@armory-sh/base';
|
|
1
2
|
import { Context, Next } from 'hono';
|
|
2
|
-
|
|
3
|
-
import { ExtensionConfig } from './extensions.js';
|
|
4
|
-
export { buildExtensions, extractExtension } from './extensions.js';
|
|
3
|
+
export { ExtensionConfig, buildExtensions, extractExtension } from './extensions.js';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
interface RouteAwarePaymentConfig extends PaymentConfig {
|
|
6
|
+
route?: string;
|
|
7
|
+
routes?: string[];
|
|
8
|
+
perRoute?: Record<string, Partial<PaymentConfig>>;
|
|
9
|
+
}
|
|
10
|
+
declare const routeAwarePaymentMiddleware: (config: RouteAwarePaymentConfig) => (c: Context, next: Next) => Promise<Response | undefined>;
|
|
10
11
|
|
|
11
12
|
type NetworkId = string | number;
|
|
12
13
|
type TokenId = string;
|
|
13
14
|
interface PaymentConfig {
|
|
14
|
-
payTo
|
|
15
|
+
payTo?: string;
|
|
16
|
+
requirements?: PaymentRequirementsV2 | PaymentRequirementsV2[];
|
|
15
17
|
chains?: NetworkId[];
|
|
16
18
|
chain?: NetworkId;
|
|
17
19
|
tokens?: TokenId[];
|
|
18
20
|
token?: TokenId;
|
|
19
21
|
amount?: string;
|
|
20
22
|
maxTimeoutSeconds?: number;
|
|
21
|
-
payToByChain?: Record<NetworkId, string>;
|
|
22
|
-
payToByToken?: Record<NetworkId, Record<TokenId, string>>;
|
|
23
23
|
facilitatorUrl?: string;
|
|
24
|
-
facilitatorUrlByChain?: Record<
|
|
25
|
-
facilitatorUrlByToken?: Record<
|
|
26
|
-
extensions?:
|
|
24
|
+
facilitatorUrlByChain?: Record<string, string>;
|
|
25
|
+
facilitatorUrlByToken?: Record<string, Record<string, string>>;
|
|
26
|
+
extensions?: Record<string, unknown>;
|
|
27
27
|
}
|
|
28
|
-
interface
|
|
28
|
+
interface ResolvedRequirementsConfig {
|
|
29
29
|
requirements: PaymentRequirementsV2[];
|
|
30
|
-
error?:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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 {
|
|
43
|
-
requirements: PaymentRequirements;
|
|
44
|
-
facilitatorUrl: string;
|
|
45
|
-
network?: string;
|
|
30
|
+
error?: {
|
|
31
|
+
code: string;
|
|
32
|
+
message: string;
|
|
33
|
+
};
|
|
46
34
|
}
|
|
47
|
-
interface
|
|
35
|
+
interface AugmentedContext extends Context {
|
|
48
36
|
payment?: {
|
|
49
37
|
payload: X402PaymentPayload;
|
|
50
38
|
payerAddress: string;
|
|
51
39
|
verified: boolean;
|
|
52
40
|
};
|
|
53
41
|
}
|
|
54
|
-
declare
|
|
42
|
+
declare function resolveFacilitatorUrlFromRequirement(config: PaymentConfig, requirement: PaymentRequirementsV2): string | undefined;
|
|
43
|
+
declare function createPaymentRequirements(config: PaymentConfig): ResolvedRequirementsConfig;
|
|
44
|
+
declare function paymentMiddleware(config: PaymentConfig): (c: Context, next: Next) => Promise<Response | undefined>;
|
|
55
45
|
|
|
56
|
-
export { type
|
|
46
|
+
export { type AugmentedContext, type PaymentConfig, type ResolvedRequirementsConfig, type RouteAwarePaymentConfig, createPaymentRequirements, paymentMiddleware, resolveFacilitatorUrlFromRequirement, routeAwarePaymentMiddleware };
|
package/dist/index.js
CHANGED
|
@@ -1,294 +1,6 @@
|
|
|
1
|
-
import { buildExtensions } from './chunk-XYM6YXAQ.js';
|
|
2
1
|
export { buildExtensions, extractExtension } from './chunk-XYM6YXAQ.js';
|
|
3
|
-
import { V2_HEADERS, safeBase64Encode, decodePayloadHeader, verifyPayment, createPaymentRequiredHeaders, settlePayment, createSettlementHeaders,
|
|
2
|
+
import { matchRoute, V2_HEADERS, safeBase64Encode, decodePayloadHeader, findRequirementByAccepted, verifyPayment, createPaymentRequiredHeaders, settlePayment, createSettlementHeaders, resolveNetwork, isValidationError, resolveToken, createPaymentRequirements as createPaymentRequirements$1, PAYMENT_SIGNATURE_HEADER, PAYMENT_REQUIRED_HEADER, validateRouteConfig, TOKENS, registerToken, getSupported } from '@armory-sh/base';
|
|
4
3
|
|
|
5
|
-
var isValidationError = (value) => {
|
|
6
|
-
return typeof value === "object" && value !== null && "code" in value;
|
|
7
|
-
};
|
|
8
|
-
function ensureTokensRegistered() {
|
|
9
|
-
for (const token of Object.values(TOKENS)) {
|
|
10
|
-
try {
|
|
11
|
-
registerToken(token);
|
|
12
|
-
} catch {
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
function resolveChainTokenInputs(chains, tokens) {
|
|
17
|
-
const resolvedNetworks = [];
|
|
18
|
-
const resolvedTokens = [];
|
|
19
|
-
const errors = [];
|
|
20
|
-
const networkInputs = chains?.length ? chains : Object.keys({
|
|
21
|
-
ethereum: 1,
|
|
22
|
-
base: 8453,
|
|
23
|
-
"base-sepolia": 84532,
|
|
24
|
-
"skale-base": 1187947933,
|
|
25
|
-
"skale-base-sepolia": 324705682,
|
|
26
|
-
"ethereum-sepolia": 11155111
|
|
27
|
-
});
|
|
28
|
-
for (const networkId of networkInputs) {
|
|
29
|
-
const resolved = resolveNetwork(networkId);
|
|
30
|
-
if (isValidationError(resolved)) {
|
|
31
|
-
errors.push(`Network "${networkId}": ${resolved.message}`);
|
|
32
|
-
} else {
|
|
33
|
-
resolvedNetworks.push(resolved);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const tokenInputs = tokens?.length ? tokens : ["usdc"];
|
|
37
|
-
for (const tokenId of tokenInputs) {
|
|
38
|
-
let found = false;
|
|
39
|
-
for (const network of resolvedNetworks) {
|
|
40
|
-
const resolved = resolveToken(tokenId, network);
|
|
41
|
-
if (isValidationError(resolved)) {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
resolvedTokens.push(resolved);
|
|
45
|
-
found = true;
|
|
46
|
-
break;
|
|
47
|
-
}
|
|
48
|
-
if (!found) {
|
|
49
|
-
errors.push(`Token "${tokenId}" not found on any specified network`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (errors.length > 0) {
|
|
53
|
-
return {
|
|
54
|
-
networks: resolvedNetworks,
|
|
55
|
-
tokens: resolvedTokens,
|
|
56
|
-
error: {
|
|
57
|
-
code: "VALIDATION_FAILED",
|
|
58
|
-
message: errors.join("; ")
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
return { networks: resolvedNetworks, tokens: resolvedTokens };
|
|
63
|
-
}
|
|
64
|
-
function toAtomicUnits(amount) {
|
|
65
|
-
if (amount.includes(".")) {
|
|
66
|
-
const [whole, fractional = ""] = amount.split(".");
|
|
67
|
-
const paddedFractional = fractional.padEnd(6, "0").slice(0, 6);
|
|
68
|
-
return `${whole}${paddedFractional}`.replace(/^0+/, "") || "0";
|
|
69
|
-
}
|
|
70
|
-
return `${amount}000000`;
|
|
71
|
-
}
|
|
72
|
-
function resolvePayTo(config, network, token) {
|
|
73
|
-
const chainId = network.config.chainId;
|
|
74
|
-
if (config.payToByToken) {
|
|
75
|
-
for (const [chainKey, tokenMap] of Object.entries(config.payToByToken)) {
|
|
76
|
-
const resolvedChain = resolveNetwork(chainKey);
|
|
77
|
-
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
78
|
-
for (const [tokenKey, address] of Object.entries(tokenMap)) {
|
|
79
|
-
const resolvedToken = resolveToken(tokenKey, network);
|
|
80
|
-
if (!isValidationError(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === token.config.contractAddress.toLowerCase()) {
|
|
81
|
-
return address;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (config.payToByChain) {
|
|
88
|
-
for (const [chainKey, address] of Object.entries(config.payToByChain)) {
|
|
89
|
-
const resolvedChain = resolveNetwork(chainKey);
|
|
90
|
-
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
91
|
-
return address;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return config.payTo;
|
|
96
|
-
}
|
|
97
|
-
function resolveFacilitatorUrl(config, network, token) {
|
|
98
|
-
const chainId = network.config.chainId;
|
|
99
|
-
if (config.facilitatorUrlByToken) {
|
|
100
|
-
for (const [chainKey, tokenMap] of Object.entries(config.facilitatorUrlByToken)) {
|
|
101
|
-
const resolvedChain = resolveNetwork(chainKey);
|
|
102
|
-
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
103
|
-
for (const [tokenKey, url] of Object.entries(tokenMap)) {
|
|
104
|
-
const resolvedToken = resolveToken(tokenKey, network);
|
|
105
|
-
if (!isValidationError(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === token.config.contractAddress.toLowerCase()) {
|
|
106
|
-
return url;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
if (config.facilitatorUrlByChain) {
|
|
113
|
-
for (const [chainKey, url] of Object.entries(config.facilitatorUrlByChain)) {
|
|
114
|
-
const resolvedChain = resolveNetwork(chainKey);
|
|
115
|
-
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
116
|
-
return url;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return config.facilitatorUrl;
|
|
121
|
-
}
|
|
122
|
-
function resolveFacilitatorUrlFromRequirement(config, requirement) {
|
|
123
|
-
const chainId = parseInt(requirement.network.split(":")[1] || "0", 10);
|
|
124
|
-
const assetAddress = requirement.asset.toLowerCase();
|
|
125
|
-
if (config.facilitatorUrlByToken) {
|
|
126
|
-
for (const [chainKey, tokenMap] of Object.entries(config.facilitatorUrlByToken)) {
|
|
127
|
-
const resolvedChain = resolveNetwork(chainKey);
|
|
128
|
-
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
129
|
-
for (const [, url] of Object.entries(tokenMap)) {
|
|
130
|
-
const network = resolveNetwork(chainKey);
|
|
131
|
-
if (!isValidationError(network)) {
|
|
132
|
-
for (const tokenKey of Object.keys(tokenMap)) {
|
|
133
|
-
const resolvedToken = resolveToken(tokenKey, network);
|
|
134
|
-
if (!isValidationError(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === assetAddress) {
|
|
135
|
-
return url;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
if (config.facilitatorUrlByChain) {
|
|
144
|
-
for (const [chainKey, url] of Object.entries(config.facilitatorUrlByChain)) {
|
|
145
|
-
const resolvedChain = resolveNetwork(chainKey);
|
|
146
|
-
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
147
|
-
return url;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return config.facilitatorUrl;
|
|
152
|
-
}
|
|
153
|
-
function createPaymentRequirements(config) {
|
|
154
|
-
ensureTokensRegistered();
|
|
155
|
-
const {
|
|
156
|
-
payTo,
|
|
157
|
-
chains,
|
|
158
|
-
chain,
|
|
159
|
-
tokens,
|
|
160
|
-
token,
|
|
161
|
-
amount = "1.0",
|
|
162
|
-
maxTimeoutSeconds = 300
|
|
163
|
-
} = config;
|
|
164
|
-
const chainInputs = chain ? [chain] : chains;
|
|
165
|
-
const tokenInputs = token ? [token] : tokens;
|
|
166
|
-
const { networks, tokens: resolvedTokens, error } = resolveChainTokenInputs(
|
|
167
|
-
chainInputs,
|
|
168
|
-
tokenInputs
|
|
169
|
-
);
|
|
170
|
-
if (error) {
|
|
171
|
-
return { requirements: [], error };
|
|
172
|
-
}
|
|
173
|
-
const requirements = [];
|
|
174
|
-
for (const network of networks) {
|
|
175
|
-
for (const resolvedToken of resolvedTokens) {
|
|
176
|
-
if (resolvedToken.network.config.chainId !== network.config.chainId) {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
const atomicAmount = toAtomicUnits(amount);
|
|
180
|
-
const tokenConfig = resolvedToken.config;
|
|
181
|
-
const resolvedPayTo = resolvePayTo(config, network, resolvedToken);
|
|
182
|
-
resolveFacilitatorUrl(config, network, resolvedToken);
|
|
183
|
-
requirements.push({
|
|
184
|
-
scheme: "exact",
|
|
185
|
-
network: network.caip2,
|
|
186
|
-
amount: atomicAmount,
|
|
187
|
-
asset: tokenConfig.contractAddress,
|
|
188
|
-
payTo: resolvedPayTo,
|
|
189
|
-
maxTimeoutSeconds,
|
|
190
|
-
extra: {
|
|
191
|
-
name: tokenConfig.name,
|
|
192
|
-
version: tokenConfig.version
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return { requirements };
|
|
198
|
-
}
|
|
199
|
-
function paymentMiddleware(config) {
|
|
200
|
-
const { requirements, error } = createPaymentRequirements(config);
|
|
201
|
-
console.log("[payment-middleware] Initialized with requirements:", JSON.stringify(requirements, null, 2));
|
|
202
|
-
return async (c, next) => {
|
|
203
|
-
if (error) {
|
|
204
|
-
console.log("[payment-middleware] Configuration error:", error.message);
|
|
205
|
-
c.status(500);
|
|
206
|
-
return c.json({
|
|
207
|
-
error: "Payment middleware configuration error",
|
|
208
|
-
details: error.message
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
const paymentHeader = c.req.header(V2_HEADERS.PAYMENT_SIGNATURE);
|
|
212
|
-
console.log("[payment-middleware] Payment header present:", !!paymentHeader);
|
|
213
|
-
if (!paymentHeader) {
|
|
214
|
-
const resource = {
|
|
215
|
-
url: c.req.url,
|
|
216
|
-
description: "API Access",
|
|
217
|
-
mimeType: "application/json"
|
|
218
|
-
};
|
|
219
|
-
const paymentRequired = {
|
|
220
|
-
x402Version: 2,
|
|
221
|
-
error: "Payment required",
|
|
222
|
-
resource,
|
|
223
|
-
accepts: requirements,
|
|
224
|
-
extensions: config.extensions ? buildExtensions(config.extensions) : void 0
|
|
225
|
-
};
|
|
226
|
-
console.log("[payment-middleware] Returning 402 with requirements:", JSON.stringify(paymentRequired.accepts, null, 2));
|
|
227
|
-
c.status(402);
|
|
228
|
-
c.header(V2_HEADERS.PAYMENT_REQUIRED, safeBase64Encode(JSON.stringify(paymentRequired)));
|
|
229
|
-
return c.json({
|
|
230
|
-
error: "Payment required",
|
|
231
|
-
accepts: requirements
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
const primaryRequirement = requirements[0];
|
|
235
|
-
if (!primaryRequirement) {
|
|
236
|
-
console.log("[payment-middleware] No requirements configured");
|
|
237
|
-
c.status(500);
|
|
238
|
-
return c.json({ error: "Payment middleware configuration error", details: "No payment requirements configured" });
|
|
239
|
-
}
|
|
240
|
-
console.log("[payment-middleware] Primary requirement:", JSON.stringify(primaryRequirement, null, 2));
|
|
241
|
-
let parsedPayload;
|
|
242
|
-
try {
|
|
243
|
-
parsedPayload = decodePayloadHeader(paymentHeader, {
|
|
244
|
-
accepted: primaryRequirement
|
|
245
|
-
});
|
|
246
|
-
} catch {
|
|
247
|
-
c.status(400);
|
|
248
|
-
return c.json({ error: "Invalid payment payload" });
|
|
249
|
-
}
|
|
250
|
-
const facilitatorUrl = resolveFacilitatorUrlFromRequirement(config, primaryRequirement);
|
|
251
|
-
console.log("[payment-middleware] Facilitator URL:", facilitatorUrl);
|
|
252
|
-
if (!facilitatorUrl) {
|
|
253
|
-
console.log("[payment-middleware] No facilitator URL configured");
|
|
254
|
-
c.status(500);
|
|
255
|
-
return c.json({ error: "Payment middleware configuration error", message: "Facilitator URL is required for verification" });
|
|
256
|
-
}
|
|
257
|
-
console.log("[payment-middleware] Verifying payment with facilitator...");
|
|
258
|
-
const verifyResult = await verifyPayment(parsedPayload, primaryRequirement, { url: facilitatorUrl });
|
|
259
|
-
console.log("[payment-middleware] Verify result:", JSON.stringify(verifyResult, null, 2));
|
|
260
|
-
if (!verifyResult.isValid) {
|
|
261
|
-
console.log("[payment-middleware] Verification failed:", verifyResult.invalidReason);
|
|
262
|
-
c.status(402);
|
|
263
|
-
c.header(V2_HEADERS.PAYMENT_REQUIRED, createPaymentRequiredHeaders(requirements)[V2_HEADERS.PAYMENT_REQUIRED]);
|
|
264
|
-
return c.json({ error: "Payment verification failed", message: verifyResult.invalidReason });
|
|
265
|
-
}
|
|
266
|
-
console.log("[payment-middleware] Payment verified, setting payment context");
|
|
267
|
-
c.set("payment", {
|
|
268
|
-
payload: parsedPayload,
|
|
269
|
-
payerAddress: verifyResult.payer,
|
|
270
|
-
verified: true
|
|
271
|
-
});
|
|
272
|
-
await next();
|
|
273
|
-
if (c.res.status >= 400) {
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
console.log("[payment-middleware] Settling payment...");
|
|
277
|
-
const settleResult = await settlePayment(parsedPayload, primaryRequirement, { url: facilitatorUrl });
|
|
278
|
-
console.log("[payment-middleware] Settle result:", JSON.stringify(settleResult, null, 2));
|
|
279
|
-
if (!settleResult.success) {
|
|
280
|
-
console.log("[payment-middleware] Settlement failed:", settleResult.errorReason);
|
|
281
|
-
c.status(502);
|
|
282
|
-
c.res = c.json({ error: "Settlement failed", details: settleResult.errorReason }, 502);
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
console.log("[payment-middleware] Payment settled, tx:", settleResult.transaction);
|
|
286
|
-
const headers = createSettlementHeaders(settleResult);
|
|
287
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
288
|
-
c.header(key, value);
|
|
289
|
-
}
|
|
290
|
-
};
|
|
291
|
-
}
|
|
292
4
|
var resolveRouteConfig = (config) => {
|
|
293
5
|
const validationError = validateRouteConfig(config);
|
|
294
6
|
if (validationError) {
|
|
@@ -323,7 +35,8 @@ var routeAwarePaymentMiddleware = (config) => {
|
|
|
323
35
|
const path = new URL(c.req.url).pathname;
|
|
324
36
|
const matchedRoute = routes.find((r) => matchRoute(r.pattern, path));
|
|
325
37
|
if (!matchedRoute) {
|
|
326
|
-
|
|
38
|
+
await next();
|
|
39
|
+
return;
|
|
327
40
|
}
|
|
328
41
|
const { requirements, error: requirementsError } = createPaymentRequirements(matchedRoute.config);
|
|
329
42
|
if (requirementsError) {
|
|
@@ -359,7 +72,10 @@ var routeAwarePaymentMiddleware = (config) => {
|
|
|
359
72
|
const primaryRequirement = requirements[0];
|
|
360
73
|
if (!primaryRequirement) {
|
|
361
74
|
c.status(500);
|
|
362
|
-
return c.json({
|
|
75
|
+
return c.json({
|
|
76
|
+
error: "Payment middleware configuration error",
|
|
77
|
+
details: "No payment requirements configured"
|
|
78
|
+
});
|
|
363
79
|
}
|
|
364
80
|
let parsedPayload;
|
|
365
81
|
try {
|
|
@@ -370,16 +86,43 @@ var routeAwarePaymentMiddleware = (config) => {
|
|
|
370
86
|
c.status(400);
|
|
371
87
|
return c.json({ error: "Invalid payment payload" });
|
|
372
88
|
}
|
|
373
|
-
const
|
|
89
|
+
const selectedRequirement = findRequirementByAccepted(
|
|
90
|
+
requirements,
|
|
91
|
+
parsedPayload.accepted
|
|
92
|
+
);
|
|
93
|
+
if (!selectedRequirement) {
|
|
94
|
+
c.status(400);
|
|
95
|
+
return c.json({
|
|
96
|
+
error: "Invalid payment payload",
|
|
97
|
+
message: "Accepted requirement is not configured for this route"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const facilitatorUrl = matchedRoute.config.facilitatorUrl ?? resolveFacilitatorUrlFromRequirement(
|
|
101
|
+
matchedRoute.config,
|
|
102
|
+
selectedRequirement
|
|
103
|
+
);
|
|
374
104
|
if (!facilitatorUrl) {
|
|
375
105
|
c.status(500);
|
|
376
|
-
return c.json({
|
|
106
|
+
return c.json({
|
|
107
|
+
error: "Payment middleware configuration error",
|
|
108
|
+
message: "Facilitator URL is required for verification"
|
|
109
|
+
});
|
|
377
110
|
}
|
|
378
|
-
const verifyResult = await verifyPayment(
|
|
111
|
+
const verifyResult = await verifyPayment(
|
|
112
|
+
parsedPayload,
|
|
113
|
+
selectedRequirement,
|
|
114
|
+
{ url: facilitatorUrl }
|
|
115
|
+
);
|
|
379
116
|
if (!verifyResult.isValid) {
|
|
380
117
|
c.status(402);
|
|
381
|
-
c.header(
|
|
382
|
-
|
|
118
|
+
c.header(
|
|
119
|
+
V2_HEADERS.PAYMENT_REQUIRED,
|
|
120
|
+
createPaymentRequiredHeaders(requirements)[V2_HEADERS.PAYMENT_REQUIRED]
|
|
121
|
+
);
|
|
122
|
+
return c.json({
|
|
123
|
+
error: "Payment verification failed",
|
|
124
|
+
message: verifyResult.invalidReason
|
|
125
|
+
});
|
|
383
126
|
}
|
|
384
127
|
c.set("payment", {
|
|
385
128
|
payload: parsedPayload,
|
|
@@ -391,10 +134,17 @@ var routeAwarePaymentMiddleware = (config) => {
|
|
|
391
134
|
if (c.res.status >= 400) {
|
|
392
135
|
return;
|
|
393
136
|
}
|
|
394
|
-
const settleResult = await settlePayment(
|
|
137
|
+
const settleResult = await settlePayment(
|
|
138
|
+
parsedPayload,
|
|
139
|
+
selectedRequirement,
|
|
140
|
+
{ url: facilitatorUrl }
|
|
141
|
+
);
|
|
395
142
|
if (!settleResult.success) {
|
|
396
143
|
c.status(502);
|
|
397
|
-
c.res = c.json(
|
|
144
|
+
c.res = c.json(
|
|
145
|
+
{ error: "Settlement failed", details: settleResult.errorReason },
|
|
146
|
+
502
|
|
147
|
+
);
|
|
398
148
|
return;
|
|
399
149
|
}
|
|
400
150
|
const headers = createSettlementHeaders(settleResult);
|
|
@@ -405,67 +155,228 @@ var routeAwarePaymentMiddleware = (config) => {
|
|
|
405
155
|
};
|
|
406
156
|
|
|
407
157
|
// src/index.ts
|
|
408
|
-
var
|
|
409
|
-
|
|
158
|
+
var extensionCapabilityCache = /* @__PURE__ */ new Map();
|
|
159
|
+
var EXTENSION_CAPABILITY_TTL_MS = 5 * 60 * 1e3;
|
|
160
|
+
function ensureTokensRegistered() {
|
|
161
|
+
for (const token of Object.values(TOKENS)) {
|
|
162
|
+
try {
|
|
163
|
+
registerToken(token);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function resolveFacilitatorUrlFromRequirement(config, requirement) {
|
|
169
|
+
const chainId = parseInt(requirement.network.split(":")[1] || "0", 10);
|
|
170
|
+
const assetAddress = requirement.asset.toLowerCase();
|
|
171
|
+
if (config.facilitatorUrlByToken) {
|
|
172
|
+
for (const [chainKey, tokenMap] of Object.entries(
|
|
173
|
+
config.facilitatorUrlByToken
|
|
174
|
+
)) {
|
|
175
|
+
const resolvedChain = resolveNetwork(chainKey);
|
|
176
|
+
if (isValidationError(resolvedChain) || resolvedChain.config.chainId !== chainId) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
for (const [tokenKey, url] of Object.entries(tokenMap)) {
|
|
180
|
+
const resolvedToken = resolveToken(tokenKey, resolvedChain);
|
|
181
|
+
if (!isValidationError(resolvedToken) && resolvedToken.config.contractAddress.toLowerCase() === assetAddress) {
|
|
182
|
+
return url;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (config.facilitatorUrlByChain) {
|
|
188
|
+
for (const [chainKey, url] of Object.entries(
|
|
189
|
+
config.facilitatorUrlByChain
|
|
190
|
+
)) {
|
|
191
|
+
const resolvedChain = resolveNetwork(chainKey);
|
|
192
|
+
if (!isValidationError(resolvedChain) && resolvedChain.config.chainId === chainId) {
|
|
193
|
+
return url;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return config.facilitatorUrl;
|
|
198
|
+
}
|
|
199
|
+
function createPaymentRequirements(config) {
|
|
200
|
+
if (config.requirements) {
|
|
201
|
+
return {
|
|
202
|
+
requirements: Array.isArray(config.requirements) ? config.requirements : [config.requirements]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (!config.payTo) {
|
|
206
|
+
return {
|
|
207
|
+
requirements: [],
|
|
208
|
+
error: {
|
|
209
|
+
code: "VALIDATION_FAILED",
|
|
210
|
+
message: "Missing payment configuration: provide payTo or explicit requirements"
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
ensureTokensRegistered();
|
|
215
|
+
return createPaymentRequirements$1(config);
|
|
216
|
+
}
|
|
217
|
+
async function resolvePaymentRequiredExtensions(config, requirements) {
|
|
218
|
+
if (!config.extensions) {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
let filtered = { ...config.extensions };
|
|
222
|
+
for (const requirement of requirements) {
|
|
223
|
+
const facilitatorUrl = resolveFacilitatorUrlFromRequirement(config, requirement);
|
|
224
|
+
if (!facilitatorUrl) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const cacheKey = `${facilitatorUrl}|${requirement.network.toLowerCase()}`;
|
|
228
|
+
const now = Date.now();
|
|
229
|
+
let keys = extensionCapabilityCache.get(cacheKey);
|
|
230
|
+
if (!keys || keys.expiresAt <= now) {
|
|
231
|
+
try {
|
|
232
|
+
const supported = await getSupported({ url: facilitatorUrl });
|
|
233
|
+
const nextKeys = /* @__PURE__ */ new Set();
|
|
234
|
+
for (const kind of supported.kinds) {
|
|
235
|
+
if (kind.network.toLowerCase() !== requirement.network.toLowerCase()) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (kind.extra && typeof kind.extra === "object") {
|
|
239
|
+
for (const key of Object.keys(kind.extra)) {
|
|
240
|
+
nextKeys.add(key);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
keys = { expiresAt: now + EXTENSION_CAPABILITY_TTL_MS, keys: nextKeys };
|
|
245
|
+
} catch {
|
|
246
|
+
keys = { expiresAt: now + EXTENSION_CAPABILITY_TTL_MS, keys: /* @__PURE__ */ new Set() };
|
|
247
|
+
}
|
|
248
|
+
extensionCapabilityCache.set(cacheKey, keys);
|
|
249
|
+
}
|
|
250
|
+
filtered = Object.fromEntries(
|
|
251
|
+
Object.entries(filtered).filter(([key]) => keys.keys.has(key))
|
|
252
|
+
);
|
|
253
|
+
if (Object.keys(filtered).length === 0) {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return filtered;
|
|
258
|
+
}
|
|
259
|
+
function paymentMiddleware(config) {
|
|
260
|
+
const { requirements, error } = createPaymentRequirements(config);
|
|
261
|
+
const resolvePaymentRequiredHeader = async () => createPaymentRequiredHeaders(requirements, {
|
|
262
|
+
extensions: await resolvePaymentRequiredExtensions(
|
|
263
|
+
config,
|
|
264
|
+
requirements
|
|
265
|
+
)
|
|
266
|
+
})[PAYMENT_REQUIRED_HEADER];
|
|
410
267
|
return async (c, next) => {
|
|
268
|
+
if (error) {
|
|
269
|
+
c.status(500);
|
|
270
|
+
return c.json({
|
|
271
|
+
error: "Payment middleware configuration error",
|
|
272
|
+
details: error.message
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
const primaryRequirement = requirements[0];
|
|
276
|
+
if (!primaryRequirement) {
|
|
277
|
+
c.status(500);
|
|
278
|
+
return c.json({
|
|
279
|
+
error: "Payment middleware configuration error",
|
|
280
|
+
details: "No payment requirements configured"
|
|
281
|
+
});
|
|
282
|
+
}
|
|
411
283
|
const paymentHeader = c.req.header(PAYMENT_SIGNATURE_HEADER);
|
|
412
284
|
if (!paymentHeader) {
|
|
413
|
-
const
|
|
285
|
+
const resource = {
|
|
286
|
+
url: c.req.url,
|
|
287
|
+
description: "API Access",
|
|
288
|
+
mimeType: "application/json"
|
|
289
|
+
};
|
|
290
|
+
const paymentRequired = {
|
|
291
|
+
x402Version: 2,
|
|
292
|
+
error: "Payment required",
|
|
293
|
+
resource,
|
|
294
|
+
accepts: requirements
|
|
295
|
+
};
|
|
414
296
|
c.status(402);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
297
|
+
c.header(
|
|
298
|
+
PAYMENT_REQUIRED_HEADER,
|
|
299
|
+
await resolvePaymentRequiredHeader() ?? safeBase64Encode(JSON.stringify(paymentRequired))
|
|
300
|
+
);
|
|
418
301
|
return c.json({
|
|
419
302
|
error: "Payment required",
|
|
420
|
-
accepts:
|
|
303
|
+
accepts: requirements
|
|
421
304
|
});
|
|
422
305
|
}
|
|
423
|
-
let
|
|
306
|
+
let parsedPayload;
|
|
424
307
|
try {
|
|
425
|
-
|
|
426
|
-
accepted:
|
|
308
|
+
parsedPayload = decodePayloadHeader(paymentHeader, {
|
|
309
|
+
accepted: primaryRequirement
|
|
427
310
|
});
|
|
428
|
-
} catch
|
|
311
|
+
} catch {
|
|
312
|
+
c.status(400);
|
|
313
|
+
return c.json({ error: "Invalid payment payload" });
|
|
314
|
+
}
|
|
315
|
+
const selectedRequirement = findRequirementByAccepted(
|
|
316
|
+
requirements,
|
|
317
|
+
parsedPayload.accepted
|
|
318
|
+
);
|
|
319
|
+
if (!selectedRequirement) {
|
|
429
320
|
c.status(400);
|
|
430
|
-
return c.json({
|
|
321
|
+
return c.json({
|
|
322
|
+
error: "Invalid payment payload",
|
|
323
|
+
message: "Accepted requirement is not configured for this endpoint"
|
|
324
|
+
});
|
|
431
325
|
}
|
|
326
|
+
const facilitatorUrl = resolveFacilitatorUrlFromRequirement(
|
|
327
|
+
config,
|
|
328
|
+
selectedRequirement
|
|
329
|
+
);
|
|
432
330
|
if (!facilitatorUrl) {
|
|
433
331
|
c.status(500);
|
|
434
|
-
return c.json({
|
|
332
|
+
return c.json({
|
|
333
|
+
error: "Payment middleware configuration error",
|
|
334
|
+
message: "Facilitator URL is required for verification"
|
|
335
|
+
});
|
|
435
336
|
}
|
|
436
|
-
const verifyResult = await verifyPayment(
|
|
337
|
+
const verifyResult = await verifyPayment(
|
|
338
|
+
parsedPayload,
|
|
339
|
+
selectedRequirement,
|
|
340
|
+
{ url: facilitatorUrl }
|
|
341
|
+
);
|
|
437
342
|
if (!verifyResult.isValid) {
|
|
438
|
-
const requiredHeaders = createPaymentRequiredHeaders(requirements);
|
|
439
343
|
c.status(402);
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
344
|
+
c.header(
|
|
345
|
+
PAYMENT_REQUIRED_HEADER,
|
|
346
|
+
await resolvePaymentRequiredHeader()
|
|
347
|
+
);
|
|
443
348
|
return c.json({
|
|
444
349
|
error: "Payment verification failed",
|
|
445
350
|
message: verifyResult.invalidReason
|
|
446
351
|
});
|
|
447
352
|
}
|
|
448
|
-
const payerAddress = verifyResult.payer ?? paymentPayload.payload.authorization.from;
|
|
449
353
|
c.set("payment", {
|
|
450
|
-
payload:
|
|
451
|
-
payerAddress,
|
|
354
|
+
payload: parsedPayload,
|
|
355
|
+
payerAddress: verifyResult.payer,
|
|
452
356
|
verified: true
|
|
453
357
|
});
|
|
454
358
|
await next();
|
|
455
359
|
if (c.res.status >= 400) {
|
|
456
360
|
return;
|
|
457
361
|
}
|
|
458
|
-
const settleResult = await settlePayment(
|
|
362
|
+
const settleResult = await settlePayment(
|
|
363
|
+
parsedPayload,
|
|
364
|
+
selectedRequirement,
|
|
365
|
+
{ url: facilitatorUrl }
|
|
366
|
+
);
|
|
459
367
|
if (!settleResult.success) {
|
|
460
368
|
c.status(502);
|
|
461
|
-
c.res = c.json(
|
|
369
|
+
c.res = c.json(
|
|
370
|
+
{ error: "Settlement failed", details: settleResult.errorReason },
|
|
371
|
+
502
|
|
372
|
+
);
|
|
462
373
|
return;
|
|
463
374
|
}
|
|
464
|
-
const
|
|
465
|
-
for (const [key, value] of Object.entries(
|
|
375
|
+
const headers = createSettlementHeaders(settleResult);
|
|
376
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
466
377
|
c.header(key, value);
|
|
467
378
|
}
|
|
468
379
|
};
|
|
469
|
-
}
|
|
380
|
+
}
|
|
470
381
|
|
|
471
|
-
export {
|
|
382
|
+
export { createPaymentRequirements, paymentMiddleware, resolveFacilitatorUrlFromRequirement, routeAwarePaymentMiddleware };
|
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.30",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
|
|
6
6
|
"keywords": [
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"hono": "^4"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@armory-sh/base": "0.2.
|
|
56
|
-
"@armory-sh/extensions": "0.1.
|
|
55
|
+
"@armory-sh/base": "0.2.30",
|
|
56
|
+
"@armory-sh/extensions": "0.1.11"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"bun-types": "latest",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "rm -rf dist && tsup",
|
|
66
66
|
"build:dts": "rm -rf dist && tsc --declaration --skipLibCheck",
|
|
67
|
+
"lint": "bun run build",
|
|
68
|
+
"format": "bun run lint",
|
|
67
69
|
"test": "bun test"
|
|
68
70
|
}
|
|
69
71
|
}
|