@armory-sh/middleware-hono 0.3.9 → 0.3.11
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/dist/index.d.ts +55 -0
- package/dist/index.js +156 -31
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import * as hono from 'hono';
|
|
2
2
|
import { Context, Next } from 'hono';
|
|
3
3
|
import * as hono_utils_http_status from 'hono/utils/http-status';
|
|
4
|
+
import * as hono_utils_types from 'hono/utils/types';
|
|
5
|
+
import * as _armory_sh_base from '@armory-sh/base';
|
|
4
6
|
import { Address, AcceptPaymentOptions, PricingConfig } from '@armory-sh/base';
|
|
5
7
|
|
|
8
|
+
type PaymentVersion = 1 | 2;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Armory V1 Types - x402 Protocol V1 Compatible
|
|
12
|
+
*
|
|
13
|
+
* Supports both the x402 V1 specification format and legacy Armory V1 format.
|
|
14
|
+
*
|
|
15
|
+
* x402 V1 Specification: https://github.com/coinbase/x402
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Network name for x402 V1 (e.g., "base-sepolia", "ethereum-mainnet")
|
|
20
|
+
*/
|
|
21
|
+
type X402V1Network = string;
|
|
22
|
+
|
|
6
23
|
/**
|
|
7
24
|
* Simple one-line middleware API for Armory merchants
|
|
8
25
|
* Focus on DX/UX - "everything just magically works"
|
|
@@ -41,6 +58,44 @@ declare const acceptPaymentsViaArmory: (config: SimpleMiddlewareConfig & {
|
|
|
41
58
|
defaultVersion?: 1 | 2;
|
|
42
59
|
}) => (c: Context, next: Next) => Promise<(Response & hono.TypedResponse<{
|
|
43
60
|
error: string;
|
|
61
|
+
x402Version: number;
|
|
62
|
+
requirements: {
|
|
63
|
+
scheme: "exact";
|
|
64
|
+
network: X402V1Network;
|
|
65
|
+
maxAmountRequired: string;
|
|
66
|
+
asset: _armory_sh_base.Address;
|
|
67
|
+
payTo: _armory_sh_base.Address;
|
|
68
|
+
resource: string;
|
|
69
|
+
description: string;
|
|
70
|
+
mimeType?: string | undefined;
|
|
71
|
+
outputSchema?: null | undefined;
|
|
72
|
+
maxTimeoutSeconds: number;
|
|
73
|
+
extra?: {
|
|
74
|
+
[x: string]: hono_utils_types.JSONValue;
|
|
75
|
+
} | undefined;
|
|
76
|
+
} | {
|
|
77
|
+
scheme: "exact";
|
|
78
|
+
network: _armory_sh_base.CAIP2ChainId;
|
|
79
|
+
amount: string;
|
|
80
|
+
asset: _armory_sh_base.Address;
|
|
81
|
+
payTo: _armory_sh_base.Address;
|
|
82
|
+
maxTimeoutSeconds: number;
|
|
83
|
+
extra?: {
|
|
84
|
+
[x: string]: hono_utils_types.JSONValue;
|
|
85
|
+
} | undefined;
|
|
86
|
+
} | {
|
|
87
|
+
amount: string;
|
|
88
|
+
network: string;
|
|
89
|
+
contractAddress: string;
|
|
90
|
+
payTo: string;
|
|
91
|
+
expiry: number;
|
|
92
|
+
};
|
|
93
|
+
}, hono_utils_http_status.ContentfulStatusCode, "json">) | (Response & hono.TypedResponse<{
|
|
94
|
+
error: string;
|
|
95
|
+
x402Version: PaymentVersion;
|
|
96
|
+
}, hono_utils_http_status.ContentfulStatusCode, "json">) | (Response & hono.TypedResponse<{
|
|
97
|
+
error: string;
|
|
98
|
+
message: string;
|
|
44
99
|
}, hono_utils_http_status.ContentfulStatusCode, "json">) | undefined>;
|
|
45
100
|
|
|
46
101
|
export { acceptPaymentsViaArmory };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
V1_HEADERS as V1_HEADERS2,
|
|
4
|
+
V2_HEADERS as V2_HEADERS2
|
|
5
|
+
} from "@armory-sh/base";
|
|
6
|
+
|
|
1
7
|
// src/simple.ts
|
|
2
8
|
import {
|
|
3
9
|
resolveNetwork,
|
|
@@ -11,8 +17,12 @@ import {
|
|
|
11
17
|
import {
|
|
12
18
|
getNetworkConfig,
|
|
13
19
|
getNetworkByChainId,
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
normalizeNetworkName,
|
|
21
|
+
encodeX402PaymentRequiredV1,
|
|
22
|
+
encodeX402SettlementResponseV1,
|
|
23
|
+
safeBase64Encode,
|
|
24
|
+
V1_HEADERS,
|
|
25
|
+
V2_HEADERS
|
|
16
26
|
} from "@armory-sh/base";
|
|
17
27
|
var toSlug = (network) => {
|
|
18
28
|
if (typeof network === "number") {
|
|
@@ -21,7 +31,9 @@ var toSlug = (network) => {
|
|
|
21
31
|
return normalizeNetworkName(net.name);
|
|
22
32
|
}
|
|
23
33
|
if (network.startsWith("eip155:")) {
|
|
24
|
-
const
|
|
34
|
+
const chainIdStr = network.split(":")[1];
|
|
35
|
+
if (!chainIdStr) throw new Error(`Invalid eip155 format: ${network}`);
|
|
36
|
+
const chainId = parseInt(chainIdStr, 10);
|
|
25
37
|
const net = getNetworkByChainId(chainId);
|
|
26
38
|
if (!net) throw new Error(`No network found for chainId: ${chainId}`);
|
|
27
39
|
return normalizeNetworkName(net.name);
|
|
@@ -46,37 +58,62 @@ var toEip155 = (network) => {
|
|
|
46
58
|
};
|
|
47
59
|
var getNetworkName = (network) => toSlug(network);
|
|
48
60
|
var getChainId = (network) => toEip155(network);
|
|
49
|
-
var createV1Requirements = (config, expiry) => {
|
|
61
|
+
var createV1Requirements = (config, resourceUrl, expiry) => {
|
|
50
62
|
const networkName = getNetworkName(config.network);
|
|
51
63
|
const network = getNetworkConfig(networkName);
|
|
52
64
|
if (!network) throw new Error(`Unsupported network: ${networkName}`);
|
|
65
|
+
const atomicAmount = toAtomicUnits(config.amount);
|
|
53
66
|
return {
|
|
54
|
-
|
|
67
|
+
scheme: "exact",
|
|
55
68
|
network: networkName,
|
|
56
|
-
|
|
69
|
+
maxAmountRequired: atomicAmount,
|
|
70
|
+
asset: network.usdcAddress,
|
|
57
71
|
payTo: config.payTo,
|
|
58
|
-
|
|
72
|
+
resource: resourceUrl,
|
|
73
|
+
description: "API Access",
|
|
74
|
+
mimeType: "application/json",
|
|
75
|
+
maxTimeoutSeconds: 300,
|
|
76
|
+
extra: {
|
|
77
|
+
name: "USDC",
|
|
78
|
+
version: "2"
|
|
79
|
+
}
|
|
59
80
|
};
|
|
60
81
|
};
|
|
61
|
-
var
|
|
82
|
+
var toAtomicUnits = (amount) => {
|
|
83
|
+
if (amount.includes(".")) {
|
|
84
|
+
const [whole, fractional = ""] = amount.split(".");
|
|
85
|
+
const paddedFractional = fractional.padEnd(6, "0").slice(0, 6);
|
|
86
|
+
return `${whole}${paddedFractional}`.replace(/^0+/, "") || "0";
|
|
87
|
+
}
|
|
88
|
+
return `${amount}000000`;
|
|
89
|
+
};
|
|
90
|
+
var createV2Requirements = (config, resourceUrl) => {
|
|
62
91
|
const networkName = getNetworkName(config.network);
|
|
63
92
|
const network = getNetworkConfig(networkName);
|
|
64
93
|
if (!network) throw new Error(`Unsupported network: ${networkName}`);
|
|
94
|
+
const assetIdMatch = network.caipAssetId.match(/\/erc20:(0x[a-fA-F0-9]{40})$/);
|
|
95
|
+
if (!assetIdMatch) throw new Error(`Invalid CAIP asset ID format: ${network.caipAssetId}`);
|
|
96
|
+
const asset = assetIdMatch[1];
|
|
97
|
+
const atomicAmount = toAtomicUnits(config.amount);
|
|
65
98
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
99
|
+
scheme: "exact",
|
|
100
|
+
network: getChainId(config.network),
|
|
101
|
+
amount: atomicAmount,
|
|
102
|
+
asset,
|
|
103
|
+
payTo: config.payTo,
|
|
104
|
+
maxTimeoutSeconds: 300,
|
|
105
|
+
extra: {
|
|
106
|
+
name: "USDC",
|
|
107
|
+
version: "2"
|
|
108
|
+
}
|
|
72
109
|
};
|
|
73
110
|
};
|
|
74
|
-
var createPaymentRequirements = (config, version = 1) => {
|
|
111
|
+
var createPaymentRequirements = (config, version = 1, resourceUrl = "https://api.example.com") => {
|
|
75
112
|
const networkName = getNetworkName(config.network);
|
|
76
113
|
const network = getNetworkConfig(networkName);
|
|
77
114
|
if (!network) throw new Error(`Unsupported network: ${networkName}`);
|
|
78
115
|
const expiry = Math.floor(Date.now() / 1e3) + 3600;
|
|
79
|
-
return version === 1 ? createV1Requirements(config, expiry) : createV2Requirements(config,
|
|
116
|
+
return version === 1 ? createV1Requirements(config, resourceUrl, expiry) : createV2Requirements(config, resourceUrl);
|
|
80
117
|
};
|
|
81
118
|
var findHeaderValue = (headers, name) => {
|
|
82
119
|
const value = headers[name];
|
|
@@ -140,6 +177,58 @@ var verifyWithFacilitator = async (request, facilitator) => {
|
|
|
140
177
|
};
|
|
141
178
|
}
|
|
142
179
|
};
|
|
180
|
+
var createX402V1PaymentRequiredHeaders = (requirements, errorMessage = "X-PAYMENT header is required") => {
|
|
181
|
+
const paymentRequired = {
|
|
182
|
+
x402Version: 1,
|
|
183
|
+
error: errorMessage,
|
|
184
|
+
accepts: [requirements]
|
|
185
|
+
};
|
|
186
|
+
return {
|
|
187
|
+
[V1_HEADERS.PAYMENT_REQUIRED]: encodeX402PaymentRequiredV1(paymentRequired),
|
|
188
|
+
"Content-Type": "application/json"
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
var createX402V2PaymentRequiredHeaders = (requirements, resourceUrl, options) => {
|
|
192
|
+
const resource = {
|
|
193
|
+
url: resourceUrl,
|
|
194
|
+
description: options?.description ?? "API Access",
|
|
195
|
+
mimeType: options?.mimeType ?? "application/json"
|
|
196
|
+
};
|
|
197
|
+
const paymentRequired = {
|
|
198
|
+
x402Version: 2,
|
|
199
|
+
...options?.errorMessage && { error: options.errorMessage },
|
|
200
|
+
resource,
|
|
201
|
+
accepts: [requirements],
|
|
202
|
+
...requirements.extra && { extensions: requirements.extra }
|
|
203
|
+
};
|
|
204
|
+
return {
|
|
205
|
+
[V2_HEADERS.PAYMENT_REQUIRED]: safeBase64Encode(JSON.stringify(paymentRequired)),
|
|
206
|
+
"Content-Type": "application/json"
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
var createSettlementHeaders = (response, version, network, payer) => {
|
|
210
|
+
if (version === 1) {
|
|
211
|
+
const txHash2 = "transaction" in response ? response.transaction : response.txHash || "";
|
|
212
|
+
const isSuccess2 = "success" in response ? response.success : false;
|
|
213
|
+
const networkName = network || "base-sepolia";
|
|
214
|
+
const settlementV1 = {
|
|
215
|
+
success: isSuccess2,
|
|
216
|
+
transaction: txHash2,
|
|
217
|
+
network: networkName,
|
|
218
|
+
payer: payer || "0x0000000000000000000000000000000000000000"
|
|
219
|
+
};
|
|
220
|
+
return { [V1_HEADERS.PAYMENT_RESPONSE]: encodeX402SettlementResponseV1(settlementV1) };
|
|
221
|
+
}
|
|
222
|
+
const txHash = "transaction" in response ? response.transaction : "";
|
|
223
|
+
const isSuccess = "success" in response ? response.success : false;
|
|
224
|
+
const settlementV2 = {
|
|
225
|
+
success: isSuccess,
|
|
226
|
+
transaction: txHash,
|
|
227
|
+
network: network || "eip155:84532",
|
|
228
|
+
...payer && { payer }
|
|
229
|
+
};
|
|
230
|
+
return { [V2_HEADERS.PAYMENT_RESPONSE]: safeBase64Encode(JSON.stringify(settlementV2)) };
|
|
231
|
+
};
|
|
143
232
|
|
|
144
233
|
// src/simple.ts
|
|
145
234
|
var findPricingConfig = (pricing, network, token, facilitatorUrl) => {
|
|
@@ -211,7 +300,6 @@ var getPrimaryConfig = (resolved) => {
|
|
|
211
300
|
import { extractPaymentFromHeaders, X402_HEADERS } from "@armory-sh/base";
|
|
212
301
|
import { verifyX402Payment as verifyPayment } from "@armory-sh/facilitator";
|
|
213
302
|
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" };
|
|
214
|
-
var encodeRequirements = (requirements) => Buffer.from(JSON.stringify(requirements)).toString("base64");
|
|
215
303
|
function isLegacyV1(payload) {
|
|
216
304
|
return typeof payload === "object" && payload !== null && "contractAddress" in payload && "network" in payload && "signature" in payload && typeof payload.signature === "string";
|
|
217
305
|
}
|
|
@@ -269,37 +357,74 @@ var acceptPaymentsViaArmory = (config) => {
|
|
|
269
357
|
}
|
|
270
358
|
const primaryConfig = getPrimaryConfig(resolved);
|
|
271
359
|
const defaultVersion = config.defaultVersion ?? 2;
|
|
272
|
-
const
|
|
360
|
+
const createRequirementsForVersion = (version, resourceUrl) => createPaymentRequirements(primaryConfig, version, resourceUrl);
|
|
361
|
+
const createPaymentRequiredResponse = (version, requirements, resourceUrl, errorMessage) => {
|
|
362
|
+
if (version === 1) {
|
|
363
|
+
return createX402V1PaymentRequiredHeaders(
|
|
364
|
+
requirements,
|
|
365
|
+
errorMessage ?? "X-PAYMENT header is required"
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
return createX402V2PaymentRequiredHeaders(
|
|
369
|
+
requirements,
|
|
370
|
+
resourceUrl,
|
|
371
|
+
{ errorMessage }
|
|
372
|
+
);
|
|
373
|
+
};
|
|
273
374
|
return async (c, next) => {
|
|
274
|
-
const paymentHeader = c.req.header(
|
|
375
|
+
const paymentHeader = c.req.header(V1_HEADERS2.PAYMENT) || c.req.header(V2_HEADERS2.PAYMENT_SIGNATURE) || c.req.header("X-Payment");
|
|
376
|
+
const resourceUrl = c.req.url;
|
|
275
377
|
if (!paymentHeader) {
|
|
276
378
|
const version = defaultVersion === 1 ? 1 : 2;
|
|
277
|
-
const requirements =
|
|
278
|
-
const headers =
|
|
379
|
+
const requirements = createRequirementsForVersion(version, resourceUrl);
|
|
380
|
+
const headers = createPaymentRequiredResponse(version, requirements, resourceUrl);
|
|
279
381
|
c.status(402);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
382
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
383
|
+
c.header(key, value);
|
|
384
|
+
}
|
|
385
|
+
return c.json({
|
|
386
|
+
error: "Payment required",
|
|
387
|
+
x402Version: version,
|
|
388
|
+
requirements
|
|
389
|
+
});
|
|
283
390
|
}
|
|
284
391
|
try {
|
|
285
392
|
const { payload, version } = decodePayload(paymentHeader);
|
|
286
393
|
if (primaryConfig.facilitator) {
|
|
287
394
|
const verifyResult = await verifyWithFacilitator(toHttpRequest(c), primaryConfig.facilitator);
|
|
288
395
|
if (!verifyResult.success) {
|
|
289
|
-
const requirements =
|
|
290
|
-
const
|
|
396
|
+
const requirements = createRequirementsForVersion(version, resourceUrl);
|
|
397
|
+
const headers = createPaymentRequiredResponse(
|
|
398
|
+
version,
|
|
399
|
+
requirements,
|
|
400
|
+
resourceUrl,
|
|
401
|
+
`Payment verification failed: ${verifyResult.error}`
|
|
402
|
+
);
|
|
291
403
|
c.status(402);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
404
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
405
|
+
c.header(key, value);
|
|
406
|
+
}
|
|
407
|
+
return c.json({
|
|
408
|
+
error: `Payment verification failed: ${verifyResult.error}`,
|
|
409
|
+
x402Version: version
|
|
410
|
+
});
|
|
295
411
|
}
|
|
296
412
|
}
|
|
297
413
|
const payerAddress = extractPayerAddress(payload);
|
|
298
|
-
const
|
|
414
|
+
const responseHeaders = getHeadersForVersion(version);
|
|
299
415
|
c.set("payment", { payload, payerAddress, version, verified: true });
|
|
300
416
|
c.header("X-Payment-Verified", "true");
|
|
301
417
|
c.header("X-Payer-Address", payerAddress);
|
|
302
|
-
|
|
418
|
+
const settlementResponse = {
|
|
419
|
+
success: true,
|
|
420
|
+
transaction: "",
|
|
421
|
+
network: version === 1 ? "base-sepolia" : "eip155:84532",
|
|
422
|
+
payer: payerAddress
|
|
423
|
+
};
|
|
424
|
+
const settlementHeaders = createSettlementHeaders(settlementResponse, version, void 0, payerAddress);
|
|
425
|
+
for (const [key, value] of Object.entries(settlementHeaders)) {
|
|
426
|
+
c.header(key, value);
|
|
427
|
+
}
|
|
303
428
|
await next();
|
|
304
429
|
} catch (error) {
|
|
305
430
|
c.status(400);
|
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.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"hono": "^4"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@armory-sh/base": "0.2.
|
|
32
|
-
"@armory-sh/facilitator": "0.2.
|
|
31
|
+
"@armory-sh/base": "0.2.12",
|
|
32
|
+
"@armory-sh/facilitator": "0.2.12"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"bun-types": "latest",
|