@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 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
- encodePaymentPayload,
15
- normalizeNetworkName
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 chainId = parseInt(network.split(":")[1], 10);
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
- amount: config.amount,
67
+ scheme: "exact",
55
68
  network: networkName,
56
- contractAddress: network.usdcAddress,
69
+ maxAmountRequired: atomicAmount,
70
+ asset: network.usdcAddress,
57
71
  payTo: config.payTo,
58
- expiry
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 createV2Requirements = (config, expiry) => {
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
- amount: config.amount,
67
- to: config.payTo,
68
- chainId: getChainId(config.network),
69
- assetId: network.caipAssetId,
70
- nonce: `${Date.now()}-${crypto.randomUUID()}`,
71
- expiry
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, expiry);
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 createRequirements = (version) => createPaymentRequirements(primaryConfig, version);
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("X-Payment") || c.req.header("x402-payment");
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 = createRequirements(version);
278
- const headers = getHeadersForVersion(version);
379
+ const requirements = createRequirementsForVersion(version, resourceUrl);
380
+ const headers = createPaymentRequiredResponse(version, requirements, resourceUrl);
279
381
  c.status(402);
280
- c.header(headers.required, encodeRequirements(requirements));
281
- c.header("Content-Type", "application/json");
282
- return c.json({ error: "Payment required", requirements });
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 = createRequirements(version);
290
- const headers2 = getHeadersForVersion(version);
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
- c.header(headers2.required, encodeRequirements(requirements));
293
- c.header("Content-Type", "application/json");
294
- return c.json({ error: `Payment verification failed: ${verifyResult.error}` });
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 headers = getHeadersForVersion(version);
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
- c.header(headers.response, JSON.stringify({ status: "verified", payerAddress, version }));
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.9",
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.9",
32
- "@armory-sh/facilitator": "0.2.9"
31
+ "@armory-sh/base": "0.2.12",
32
+ "@armory-sh/facilitator": "0.2.12"
33
33
  },
34
34
  "devDependencies": {
35
35
  "bun-types": "latest",