@armory-sh/middleware-hono 0.3.2

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.
@@ -0,0 +1,46 @@
1
+ import * as hono from 'hono';
2
+ import { Context, Next } from 'hono';
3
+ import * as hono_utils_http_status from 'hono/utils/http-status';
4
+ import { Address, AcceptPaymentOptions, PricingConfig } from '@armory-sh/base';
5
+
6
+ /**
7
+ * Simple one-line middleware API for Armory merchants
8
+ * Focus on DX/UX - "everything just magically works"
9
+ */
10
+
11
+ /**
12
+ * Simple configuration for acceptPaymentsViaArmory middleware
13
+ */
14
+ interface SimpleMiddlewareConfig {
15
+ /** Address to receive payments */
16
+ payTo: Address;
17
+ /** Default amount to charge (default: "1.0") */
18
+ amount?: string;
19
+ /** Payment acceptance options */
20
+ accept?: AcceptPaymentOptions;
21
+ /** Fallback facilitator URL (if not using accept.facilitators) */
22
+ facilitatorUrl?: string;
23
+ /** Per-network/token/facilitator pricing overrides */
24
+ pricing?: PricingConfig[];
25
+ }
26
+
27
+ /**
28
+ * One-line middleware setup for accepting Armory payments in Hono
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * import { acceptPaymentsViaArmory } from '@armory-sh/middleware/integrations/hono-simple'
33
+ *
34
+ * app.use('/api/*', acceptPaymentsViaArmory({
35
+ * payTo: '0xMerchantAddress...',
36
+ * amount: '1.0'
37
+ * }))
38
+ * ```
39
+ */
40
+ declare const acceptPaymentsViaArmory: (config: SimpleMiddlewareConfig & {
41
+ defaultVersion?: 1 | 2;
42
+ }) => (c: Context, next: Next) => Promise<(Response & hono.TypedResponse<{
43
+ error: string;
44
+ }, hono_utils_http_status.ContentfulStatusCode, "json">) | undefined>;
45
+
46
+ export { acceptPaymentsViaArmory };
package/dist/index.js ADDED
@@ -0,0 +1,288 @@
1
+ // src/simple.ts
2
+ import {
3
+ resolveNetwork,
4
+ resolveToken,
5
+ validateAcceptConfig,
6
+ isValidationError
7
+ } from "@armory-sh/base";
8
+
9
+ // src/core.ts
10
+ import {
11
+ getNetworkConfig,
12
+ getNetworkByChainId,
13
+ encodePaymentPayload
14
+ } from "@armory-sh/base";
15
+ var getNetworkName = (network) => {
16
+ if (typeof network === "string") return network;
17
+ const net = getNetworkByChainId(network);
18
+ if (!net) throw new Error(`No network found for chainId: ${network}`);
19
+ return net.name.toLowerCase().replace(" mainnet", "").replace(" sepolia", "-sepolia");
20
+ };
21
+ var createV1Requirements = (config, expiry) => {
22
+ const networkName = getNetworkName(config.network);
23
+ const network = getNetworkConfig(networkName);
24
+ if (!network) throw new Error(`Unsupported network: ${networkName}`);
25
+ return {
26
+ amount: config.amount,
27
+ network: networkName,
28
+ contractAddress: network.usdcAddress,
29
+ payTo: config.payTo,
30
+ expiry
31
+ };
32
+ };
33
+ var createV2Requirements = (config, expiry) => {
34
+ const networkName = getNetworkName(config.network);
35
+ const network = getNetworkConfig(networkName);
36
+ if (!network) throw new Error(`Unsupported network: ${networkName}`);
37
+ return {
38
+ amount: config.amount,
39
+ to: config.payTo,
40
+ chainId: network.caip2Id,
41
+ assetId: network.caipAssetId,
42
+ nonce: `${Date.now()}-${crypto.randomUUID()}`,
43
+ expiry
44
+ };
45
+ };
46
+ var createPaymentRequirements = (config, version = 1) => {
47
+ const networkName = getNetworkName(config.network);
48
+ const network = getNetworkConfig(networkName);
49
+ if (!network) throw new Error(`Unsupported network: ${networkName}`);
50
+ const expiry = Math.floor(Date.now() / 1e3) + 3600;
51
+ return version === 1 ? createV1Requirements(config, expiry) : createV2Requirements(config, expiry);
52
+ };
53
+ var findHeaderValue = (headers, name) => {
54
+ const value = headers[name];
55
+ if (typeof value === "string") return value;
56
+ if (Array.isArray(value) && value.length > 0) return value[0];
57
+ const lowerName = name.toLowerCase();
58
+ for (const [key, val] of Object.entries(headers)) {
59
+ if (key.toLowerCase() === lowerName) {
60
+ if (typeof val === "string") return val;
61
+ if (Array.isArray(val) && val.length > 0) return val[0];
62
+ }
63
+ }
64
+ return void 0;
65
+ };
66
+ var parseHeader = (header) => {
67
+ try {
68
+ if (header.startsWith("{")) return JSON.parse(header);
69
+ return JSON.parse(atob(header));
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
74
+ var extractPaymentPayload = (request) => {
75
+ const v1Header = findHeaderValue(request.headers, "X-PAYMENT");
76
+ if (v1Header) return parseHeader(v1Header);
77
+ const v2Header = findHeaderValue(request.headers, "PAYMENT-SIGNATURE");
78
+ if (v2Header) return parseHeader(v2Header);
79
+ return null;
80
+ };
81
+ var postFacilitator = async (url, headers, body) => {
82
+ const response = await fetch(url, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json", ...headers },
85
+ body: JSON.stringify(body)
86
+ });
87
+ if (!response.ok) {
88
+ const error = await response.text();
89
+ throw new Error(error || `Request failed: ${response.status}`);
90
+ }
91
+ return response.json();
92
+ };
93
+ var verifyWithFacilitator = async (request, facilitator) => {
94
+ const payload = extractPaymentPayload(request);
95
+ if (!payload) {
96
+ return { success: false, error: "No payment payload found in request headers" };
97
+ }
98
+ try {
99
+ const url = new URL("/verify", facilitator.url);
100
+ const headers = facilitator.createHeaders?.() ?? {};
101
+ const data = await postFacilitator(url.toString(), headers, { payload, headers: request.headers });
102
+ return {
103
+ success: true,
104
+ payerAddress: data.payerAddress,
105
+ balance: data.balance,
106
+ requiredAmount: data.requiredAmount
107
+ };
108
+ } catch (error) {
109
+ return {
110
+ success: false,
111
+ error: error instanceof Error ? error.message : "Unknown verification error"
112
+ };
113
+ }
114
+ };
115
+
116
+ // src/simple.ts
117
+ var findPricingConfig = (pricing, network, token, facilitatorUrl) => {
118
+ if (!pricing) return void 0;
119
+ const withFacilitator = pricing.find(
120
+ (p) => p.network === network && p.token === token && p.facilitator === facilitatorUrl
121
+ );
122
+ if (withFacilitator) return withFacilitator;
123
+ const withNetworkToken = pricing.find(
124
+ (p) => p.network === network && p.token === token && !p.facilitator
125
+ );
126
+ if (withNetworkToken) return withNetworkToken;
127
+ const networkOnly = pricing.find(
128
+ (p) => p.network === network && !p.token && !p.facilitator
129
+ );
130
+ if (networkOnly) return networkOnly;
131
+ return void 0;
132
+ };
133
+ var resolveMiddlewareConfig = (config) => {
134
+ const { payTo, amount = "1.0", accept = {}, facilitatorUrl, pricing } = config;
135
+ const acceptOptions = facilitatorUrl ? {
136
+ ...accept,
137
+ facilitators: accept.facilitators ? [...Array.isArray(accept.facilitators) ? accept.facilitators : [accept.facilitators], { url: facilitatorUrl }] : { url: facilitatorUrl }
138
+ } : accept;
139
+ const result = validateAcceptConfig(acceptOptions, payTo, amount);
140
+ if (!result.success) {
141
+ return result.error;
142
+ }
143
+ const facilitatorConfigs = result.config[0]?.facilitators.map((f) => ({
144
+ url: f.url,
145
+ createHeaders: f.input.headers
146
+ })) ?? [];
147
+ const enrichedConfigs = result.config.map((c) => {
148
+ const networkName = c.network.config.name;
149
+ const tokenSymbol = c.token.config.symbol;
150
+ const facilitatorPricing = c.facilitators.map((f) => {
151
+ const pricingConfig = findPricingConfig(pricing, networkName, tokenSymbol, f.url);
152
+ return { url: f.url, pricing: pricingConfig };
153
+ });
154
+ const defaultPricing = findPricingConfig(pricing, networkName, tokenSymbol, "");
155
+ return {
156
+ ...c,
157
+ amount: defaultPricing?.amount ?? c.amount,
158
+ facilitatorUrl: facilitatorPricing[0]?.url,
159
+ pricing: defaultPricing
160
+ };
161
+ });
162
+ return {
163
+ configs: enrichedConfigs,
164
+ version: acceptOptions.version ?? "auto",
165
+ facilitators: facilitatorConfigs
166
+ };
167
+ };
168
+ var getPrimaryConfig = (resolved) => {
169
+ const primary = resolved.configs[0];
170
+ if (!primary) {
171
+ throw new Error("No valid payment configurations found");
172
+ }
173
+ return {
174
+ payTo: primary.payTo,
175
+ network: primary.network.config.name,
176
+ amount: primary.amount,
177
+ facilitator: resolved.facilitators[0],
178
+ settlementMode: "verify"
179
+ };
180
+ };
181
+
182
+ // src/payment-utils.ts
183
+ import { extractPaymentFromHeaders, X402_HEADERS } from "@armory-sh/base";
184
+ import { verifyX402Payment as verifyPayment } from "@armory-sh/facilitator";
185
+ var getHeadersForVersion = (version) => version === 1 ? { payment: "X-PAYMENT", required: "X-PAYMENT-REQUIRED", response: "X-PAYMENT-RESPONSE" } : { payment: "PAYMENT-SIGNATURE", required: "PAYMENT-REQUIRED", response: "PAYMENT-RESPONSE" };
186
+ var encodeRequirements = (requirements) => JSON.stringify(requirements);
187
+ function isLegacyV1(payload) {
188
+ return typeof payload === "object" && payload !== null && "contractAddress" in payload && "network" in payload && "signature" in payload && typeof payload.signature === "string";
189
+ }
190
+ function isLegacyV2(payload) {
191
+ return typeof payload === "object" && payload !== null && "chainId" in payload && "assetId" in payload && "signature" in payload && typeof payload.signature === "string";
192
+ }
193
+ var decodePayload = (headerValue) => {
194
+ let parsed;
195
+ try {
196
+ if (headerValue.startsWith("{")) {
197
+ parsed = JSON.parse(headerValue);
198
+ } else {
199
+ parsed = JSON.parse(atob(headerValue));
200
+ }
201
+ } catch {
202
+ throw new Error("Invalid payment payload");
203
+ }
204
+ const headers = new Headers();
205
+ headers.set(X402_HEADERS.PAYMENT, headerValue);
206
+ const x402Payload = extractPaymentFromHeaders(headers);
207
+ if (x402Payload) {
208
+ return { payload: x402Payload, version: 2 };
209
+ }
210
+ if (isLegacyV1(parsed)) {
211
+ return { payload: parsed, version: 1 };
212
+ }
213
+ if (isLegacyV2(parsed)) {
214
+ return { payload: parsed, version: 2 };
215
+ }
216
+ throw new Error("Unrecognized payment payload format");
217
+ };
218
+ var extractPayerAddress = (payload) => {
219
+ if ("payload" in payload) {
220
+ const x402Payload = payload;
221
+ if ("authorization" in x402Payload.payload) {
222
+ return x402Payload.payload.authorization.from;
223
+ }
224
+ }
225
+ if ("from" in payload && typeof payload.from === "string") {
226
+ return payload.from;
227
+ }
228
+ throw new Error("Unable to extract payer address from payload");
229
+ };
230
+
231
+ // src/index.ts
232
+ var toHttpRequest = (c) => ({
233
+ headers: Object.fromEntries(Object.entries(c.req.header()).map(([k, v]) => [k, v ?? ""])),
234
+ method: c.req.method,
235
+ url: c.req.url
236
+ });
237
+ var acceptPaymentsViaArmory = (config) => {
238
+ const resolved = resolveMiddlewareConfig(config);
239
+ if ("code" in resolved) {
240
+ throw new Error(`Invalid payment configuration: ${resolved.message}`);
241
+ }
242
+ const primaryConfig = getPrimaryConfig(resolved);
243
+ const defaultVersion = config.defaultVersion ?? 2;
244
+ const requirementsV1 = createPaymentRequirements(primaryConfig, 1);
245
+ const requirementsV2 = createPaymentRequirements(primaryConfig, 2);
246
+ return async (c, next) => {
247
+ const paymentHeader = c.req.header("X-Payment") || c.req.header("x402-payment");
248
+ if (!paymentHeader) {
249
+ const requirements = defaultVersion === 1 ? requirementsV1 : requirementsV2;
250
+ const version = defaultVersion === 1 ? 1 : 2;
251
+ const headers = getHeadersForVersion(version);
252
+ c.status(402);
253
+ c.header(headers.required, encodeRequirements(requirements));
254
+ c.header("Content-Type", "application/json");
255
+ return c.json({ error: "Payment required", requirements });
256
+ }
257
+ try {
258
+ const { payload, version } = decodePayload(paymentHeader);
259
+ if (primaryConfig.facilitator) {
260
+ const verifyResult = await verifyWithFacilitator(toHttpRequest(c), primaryConfig.facilitator);
261
+ if (!verifyResult.success) {
262
+ const requirements = version === 1 ? requirementsV1 : requirementsV2;
263
+ const headers2 = getHeadersForVersion(version);
264
+ c.status(402);
265
+ c.header(headers2.required, encodeRequirements(requirements));
266
+ c.header("Content-Type", "application/json");
267
+ return c.json({ error: `Payment verification failed: ${verifyResult.error}` });
268
+ }
269
+ }
270
+ const payerAddress = extractPayerAddress(payload);
271
+ const headers = getHeadersForVersion(version);
272
+ c.set("payment", { payload, payerAddress, version, verified: true });
273
+ c.header("X-Payment-Verified", "true");
274
+ c.header("X-Payer-Address", payerAddress);
275
+ c.header(headers.response, JSON.stringify({ status: "verified", payerAddress, version }));
276
+ await next();
277
+ } catch (error) {
278
+ c.status(400);
279
+ return c.json({
280
+ error: "Invalid payment payload",
281
+ message: error instanceof Error ? error.message : "Unknown error"
282
+ });
283
+ }
284
+ };
285
+ };
286
+ export {
287
+ acceptPaymentsViaArmory
288
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@armory-sh/middleware-hono",
3
+ "version": "0.3.2",
4
+ "license": "MIT",
5
+ "author": "Sawyer Cutler <sawyer@dirtroad.dev>",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "bun": "./src/index.ts",
13
+ "default": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/thegreataxios/armory.git",
25
+ "directory": "packages/middleware-hono"
26
+ },
27
+ "peerDependencies": {
28
+ "hono": "^4"
29
+ },
30
+ "dependencies": {
31
+ "@armory-sh/base": "0.2.4",
32
+ "@armory-sh/facilitator": "0.2.4"
33
+ },
34
+ "devDependencies": {
35
+ "bun-types": "latest",
36
+ "typescript": "5.9.3",
37
+ "hono": "^4"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "build:dts": "tsc --declaration --skipLibCheck",
42
+ "test": "bun test"
43
+ }
44
+ }