@armory-sh/middleware-express 0.4.1 → 0.4.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.
- package/package.json +1 -1
- package/src/index.ts +50 -49
- package/src/payment-utils.ts +61 -86
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,88 +1,89 @@
|
|
|
1
1
|
import type { Request, Response, NextFunction } from "express";
|
|
2
2
|
import type {
|
|
3
|
-
|
|
3
|
+
PaymentPayloadV2,
|
|
4
4
|
PaymentRequirements,
|
|
5
|
-
} from "
|
|
5
|
+
} from "@armory-sh/base";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
extractPayerAddress,
|
|
13
|
-
} from "./payment-utils";
|
|
7
|
+
decodePaymentV2,
|
|
8
|
+
createPaymentRequiredHeaders,
|
|
9
|
+
createSettlementHeaders,
|
|
10
|
+
PAYMENT_SIGNATURE_HEADER,
|
|
11
|
+
} from "@armory-sh/base";
|
|
14
12
|
|
|
15
13
|
export interface PaymentMiddlewareConfig {
|
|
16
14
|
requirements: PaymentRequirements;
|
|
17
15
|
facilitatorUrl?: string;
|
|
18
16
|
skipVerification?: boolean;
|
|
17
|
+
network?: string;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
export interface AugmentedRequest extends Request {
|
|
22
21
|
payment?: {
|
|
23
|
-
payload:
|
|
22
|
+
payload: PaymentPayloadV2;
|
|
24
23
|
payerAddress: string;
|
|
25
|
-
version: 1 | 2;
|
|
26
24
|
verified: boolean;
|
|
27
25
|
};
|
|
28
26
|
}
|
|
29
27
|
|
|
30
|
-
const sendError = (
|
|
31
|
-
res: Response,
|
|
32
|
-
status: number,
|
|
33
|
-
headers: Record<string, string>,
|
|
34
|
-
body: unknown
|
|
35
|
-
): void => {
|
|
36
|
-
// Express v5: use res.statusCode instead of res.status()
|
|
37
|
-
res.statusCode = status;
|
|
38
|
-
Object.entries(headers).forEach(([k, v]) => res.setHeader(k, v));
|
|
39
|
-
res.json(body);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
28
|
export const paymentMiddleware = (config: PaymentMiddlewareConfig) => {
|
|
43
|
-
const { requirements, facilitatorUrl, skipVerification = false } = config;
|
|
44
|
-
const version = getRequirementsVersion(requirements);
|
|
45
|
-
const headers = getHeadersForVersion(version);
|
|
29
|
+
const { requirements, facilitatorUrl, skipVerification = false, network = "base" } = config;
|
|
46
30
|
|
|
47
31
|
return async (req: AugmentedRequest, res: Response, next: NextFunction): Promise<void> => {
|
|
48
32
|
try {
|
|
49
|
-
const paymentHeader = req.headers[
|
|
33
|
+
const paymentHeader = req.headers[PAYMENT_SIGNATURE_HEADER.toLowerCase()] as string | undefined;
|
|
50
34
|
|
|
51
35
|
if (!paymentHeader) {
|
|
52
|
-
|
|
36
|
+
const requiredHeaders = createPaymentRequiredHeaders(requirements);
|
|
37
|
+
res.statusCode = 402;
|
|
38
|
+
for (const [key, value] of Object.entries(requiredHeaders)) {
|
|
39
|
+
res.setHeader(key, value);
|
|
40
|
+
}
|
|
41
|
+
res.json({
|
|
42
|
+
error: "Payment required",
|
|
43
|
+
accepts: [requirements],
|
|
44
|
+
});
|
|
53
45
|
return;
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
let
|
|
57
|
-
let payloadVersion: 1 | 2;
|
|
48
|
+
let paymentPayload: PaymentPayloadV2;
|
|
58
49
|
try {
|
|
59
|
-
|
|
50
|
+
paymentPayload = decodePaymentV2(paymentHeader);
|
|
60
51
|
} catch (error) {
|
|
61
|
-
|
|
52
|
+
res.statusCode = 400;
|
|
53
|
+
res.json({
|
|
54
|
+
error: "Invalid payment payload",
|
|
55
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
56
|
+
});
|
|
62
57
|
return;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
|
|
66
|
-
sendError(res, 400, {}, { error: "Payment version mismatch", expected: version, received: payloadVersion });
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
60
|
+
const payerAddress = paymentPayload.payload.authorization.from;
|
|
69
61
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
62
|
+
// TODO: Add verification with facilitator if not skipVerification
|
|
63
|
+
// if (!skipVerification && facilitatorUrl) {
|
|
64
|
+
// const result = await verifyWithFacilitator(...);
|
|
65
|
+
// if (!result.success) { ... }
|
|
66
|
+
// }
|
|
67
|
+
|
|
68
|
+
const settlement = {
|
|
69
|
+
success: true,
|
|
70
|
+
transaction: "",
|
|
71
|
+
network,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const settlementHeaders = createSettlementHeaders(settlement);
|
|
75
|
+
for (const [key, value] of Object.entries(settlementHeaders)) {
|
|
76
|
+
res.setHeader(key, value);
|
|
77
|
+
}
|
|
80
78
|
|
|
81
|
-
req.payment = { payload, payerAddress,
|
|
82
|
-
res.setHeader(headers.response, JSON.stringify({ status: "verified", payerAddress, version }));
|
|
79
|
+
req.payment = { payload: paymentPayload, payerAddress, verified: !skipVerification };
|
|
83
80
|
next();
|
|
84
81
|
} catch (error) {
|
|
85
|
-
|
|
82
|
+
res.statusCode = 500;
|
|
83
|
+
res.json({
|
|
84
|
+
error: "Payment middleware error",
|
|
85
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
86
|
+
});
|
|
86
87
|
}
|
|
87
88
|
};
|
|
88
89
|
};
|
package/src/payment-utils.ts
CHANGED
|
@@ -2,18 +2,7 @@ import type {
|
|
|
2
2
|
X402PaymentPayload,
|
|
3
3
|
X402PaymentRequirements,
|
|
4
4
|
} from "@armory-sh/base";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
// Legacy V1 payload type for backward compatibility
|
|
8
|
-
export interface LegacyPaymentPayloadV1 {
|
|
9
|
-
amount: string;
|
|
10
|
-
network: string;
|
|
11
|
-
contractAddress: string;
|
|
12
|
-
payTo: string;
|
|
13
|
-
from: string;
|
|
14
|
-
expiry: number;
|
|
15
|
-
signature: string;
|
|
16
|
-
}
|
|
5
|
+
import { decodePayment, isPaymentPayload, isExactEvmPayload } from "@armory-sh/base";
|
|
17
6
|
|
|
18
7
|
// Legacy V2 payload type for backward compatibility
|
|
19
8
|
export interface LegacyPaymentPayloadV2 {
|
|
@@ -27,51 +16,32 @@ export interface LegacyPaymentPayloadV2 {
|
|
|
27
16
|
signature: string;
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
// Union type for
|
|
31
|
-
export type AnyPaymentPayload = X402PaymentPayload |
|
|
19
|
+
// Union type for V2 payload formats
|
|
20
|
+
export type AnyPaymentPayload = X402PaymentPayload | LegacyPaymentPayloadV2;
|
|
32
21
|
|
|
33
22
|
// Re-export types for use in index.ts
|
|
34
|
-
export type PaymentPayload = AnyPaymentPayload;
|
|
35
23
|
export type { X402PaymentRequirements as PaymentRequirements } from "@armory-sh/base";
|
|
36
24
|
|
|
37
|
-
export type PaymentVersion = 1 | 2;
|
|
38
|
-
|
|
39
25
|
export interface PaymentVerificationResult {
|
|
40
26
|
success: boolean;
|
|
41
27
|
payload?: AnyPaymentPayload;
|
|
42
|
-
version?: PaymentVersion;
|
|
43
28
|
payerAddress?: string;
|
|
44
29
|
error?: string;
|
|
45
30
|
}
|
|
46
31
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
export const getRequirementsVersion = (requirements: X402PaymentRequirements): PaymentVersion =>
|
|
59
|
-
"contractAddress" in requirements && "network" in requirements ? 1 : 2;
|
|
60
|
-
|
|
61
|
-
export const encodeRequirements = (requirements: X402PaymentRequirements, _resourceUrl?: string): string =>
|
|
32
|
+
// V2 hardcoded headers
|
|
33
|
+
export const PAYMENT_HEADERS = {
|
|
34
|
+
PAYMENT: "PAYMENT-SIGNATURE",
|
|
35
|
+
REQUIRED: "PAYMENT-REQUIRED",
|
|
36
|
+
RESPONSE: "PAYMENT-RESPONSE",
|
|
37
|
+
payment: "PAYMENT-SIGNATURE",
|
|
38
|
+
required: "PAYMENT-REQUIRED",
|
|
39
|
+
response: "PAYMENT-RESPONSE",
|
|
40
|
+
} as const;
|
|
41
|
+
|
|
42
|
+
export const encodeRequirements = (requirements: X402PaymentRequirements): string =>
|
|
62
43
|
JSON.stringify(requirements);
|
|
63
44
|
|
|
64
|
-
function isLegacyV1(payload: unknown): payload is LegacyPaymentPayloadV1 {
|
|
65
|
-
return (
|
|
66
|
-
typeof payload === "object" &&
|
|
67
|
-
payload !== null &&
|
|
68
|
-
"contractAddress" in payload &&
|
|
69
|
-
"network" in payload &&
|
|
70
|
-
"signature" in payload &&
|
|
71
|
-
typeof (payload as LegacyPaymentPayloadV1).signature === "string"
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
45
|
function isLegacyV2(payload: unknown): payload is LegacyPaymentPayloadV2 {
|
|
76
46
|
return (
|
|
77
47
|
typeof payload === "object" &&
|
|
@@ -85,39 +55,52 @@ function isLegacyV2(payload: unknown): payload is LegacyPaymentPayloadV2 {
|
|
|
85
55
|
|
|
86
56
|
export const decodePayload = (
|
|
87
57
|
headerValue: string
|
|
88
|
-
): { payload: AnyPaymentPayload
|
|
89
|
-
let
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
58
|
+
): { payload: AnyPaymentPayload } => {
|
|
59
|
+
let payload: unknown;
|
|
60
|
+
|
|
61
|
+
// Try JSON first (for test compatibility)
|
|
62
|
+
if (headerValue.startsWith("{")) {
|
|
63
|
+
try {
|
|
64
|
+
payload = JSON.parse(headerValue);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error("Invalid payment payload: not valid JSON");
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Use core's decodePayment for proper Base64URL handling
|
|
70
|
+
try {
|
|
71
|
+
payload = decodePayment(headerValue);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error("Invalid payment payload: not valid Base64");
|
|
97
74
|
}
|
|
98
|
-
} catch {
|
|
99
|
-
throw new Error("Invalid payment payload");
|
|
100
75
|
}
|
|
101
76
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const headers = new Headers();
|
|
106
|
-
headers.set(X402_HEADERS.PAYMENT, base64Value);
|
|
107
|
-
const x402Payload = extractPaymentFromHeaders(headers);
|
|
108
|
-
if (x402Payload) {
|
|
109
|
-
return { payload: x402Payload, version: 2 };
|
|
77
|
+
// Check for x402 V2 format
|
|
78
|
+
if (isPaymentPayload(payload)) {
|
|
79
|
+
return { payload };
|
|
110
80
|
}
|
|
111
81
|
|
|
112
|
-
if (
|
|
113
|
-
return { payload
|
|
82
|
+
if (isLegacyV2(payload)) {
|
|
83
|
+
return { payload };
|
|
114
84
|
}
|
|
115
85
|
|
|
116
|
-
|
|
117
|
-
|
|
86
|
+
throw new Error("Unrecognized payment payload format");
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const extractPayerAddress = (payload: AnyPaymentPayload): string => {
|
|
90
|
+
// Check for x402 format with nested payload
|
|
91
|
+
if (isPaymentPayload(payload)) {
|
|
92
|
+
const p = payload.payload;
|
|
93
|
+
if (isExactEvmPayload(p) && p.authorization?.from) {
|
|
94
|
+
return p.authorization.from;
|
|
95
|
+
}
|
|
118
96
|
}
|
|
119
97
|
|
|
120
|
-
|
|
98
|
+
// Check for direct `from` field (legacy format)
|
|
99
|
+
if ("from" in payload && typeof payload.from === "string") {
|
|
100
|
+
return payload.from;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw new Error("Unable to extract payer address from payload");
|
|
121
104
|
};
|
|
122
105
|
|
|
123
106
|
export const verifyWithFacilitator = async (
|
|
@@ -155,16 +138,23 @@ export const verifyLocally = async (
|
|
|
155
138
|
payload: AnyPaymentPayload,
|
|
156
139
|
requirements: X402PaymentRequirements
|
|
157
140
|
): Promise<PaymentVerificationResult> => {
|
|
158
|
-
if (
|
|
141
|
+
if (isLegacyV2(payload)) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
error: "Local verification not supported for legacy payload format. Use a facilitator.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!isPaymentPayload(payload) || !isExactEvmPayload(payload.payload)) {
|
|
159
149
|
return {
|
|
160
150
|
success: false,
|
|
161
|
-
error: "
|
|
151
|
+
error: "Invalid payment payload format",
|
|
162
152
|
};
|
|
163
153
|
}
|
|
164
154
|
|
|
165
155
|
return {
|
|
166
156
|
success: true,
|
|
167
|
-
payerAddress:
|
|
157
|
+
payerAddress: payload.payload.authorization.from,
|
|
168
158
|
};
|
|
169
159
|
};
|
|
170
160
|
|
|
@@ -176,18 +166,3 @@ export const verifyPaymentWithRetry = async (
|
|
|
176
166
|
facilitatorUrl
|
|
177
167
|
? verifyWithFacilitator(facilitatorUrl, payload, requirements)
|
|
178
168
|
: verifyLocally(payload, requirements);
|
|
179
|
-
|
|
180
|
-
export const extractPayerAddress = (payload: AnyPaymentPayload): string => {
|
|
181
|
-
if ("payload" in payload) {
|
|
182
|
-
const x402Payload = payload as X402PaymentPayload;
|
|
183
|
-
if ("authorization" in x402Payload.payload) {
|
|
184
|
-
return x402Payload.payload.authorization.from;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if ("from" in payload && typeof payload.from === "string") {
|
|
189
|
-
return payload.from;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
throw new Error("Unable to extract payer address from payload");
|
|
193
|
-
};
|