@buildersgarden/siwa 0.0.17 → 0.0.19
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 +1 -0
- package/dist/index.js +1 -0
- package/dist/server-side-wrappers/express.d.ts +18 -3
- package/dist/server-side-wrappers/express.js +82 -7
- package/dist/server-side-wrappers/fastify.d.ts +9 -2
- package/dist/server-side-wrappers/fastify.js +70 -4
- package/dist/server-side-wrappers/hono.d.ts +8 -2
- package/dist/server-side-wrappers/hono.js +66 -4
- package/dist/server-side-wrappers/next.d.ts +14 -5
- package/dist/server-side-wrappers/next.js +98 -18
- package/dist/x402.d.ts +140 -0
- package/dist/x402.js +161 -0
- package/package.json +5 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
import type { RequestHandler } from 'express';
|
|
18
18
|
import { type SiwaAgent, type VerifyOptions } from '../erc8128.js';
|
|
19
19
|
import type { SignerType } from '../signer/index.js';
|
|
20
|
+
import { type X402Config, type X402Payment } from '../x402.js';
|
|
20
21
|
export type { SiwaAgent };
|
|
21
22
|
declare global {
|
|
22
23
|
namespace Express {
|
|
23
24
|
interface Request {
|
|
24
25
|
agent?: SiwaAgent;
|
|
26
|
+
payment?: X402Payment;
|
|
25
27
|
rawBody?: string;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -37,6 +39,8 @@ export interface SiwaMiddlewareOptions {
|
|
|
37
39
|
publicClient?: VerifyOptions['publicClient'];
|
|
38
40
|
/** Allowed signer types. Omit to accept all. */
|
|
39
41
|
allowedSignerTypes?: SignerType[];
|
|
42
|
+
/** Optional x402 payment gate. When set, both SIWA auth AND a valid payment are required. */
|
|
43
|
+
x402?: X402Config;
|
|
40
44
|
}
|
|
41
45
|
export interface SiwaCorsOptions {
|
|
42
46
|
/** Allowed origin(s). Defaults to "*". */
|
|
@@ -45,10 +49,14 @@ export interface SiwaCorsOptions {
|
|
|
45
49
|
methods?: string;
|
|
46
50
|
/** Allowed headers. */
|
|
47
51
|
headers?: string;
|
|
52
|
+
/** Include x402 payment headers in CORS. */
|
|
53
|
+
x402?: boolean;
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
50
56
|
* CORS middleware pre-configured with SIWA-specific headers.
|
|
51
57
|
* Handles OPTIONS preflight automatically.
|
|
58
|
+
*
|
|
59
|
+
* When `x402: true` is set, also includes x402 payment headers.
|
|
52
60
|
*/
|
|
53
61
|
export declare function siwaCors(options?: SiwaCorsOptions): RequestHandler;
|
|
54
62
|
/**
|
|
@@ -56,9 +64,16 @@ export declare function siwaCors(options?: SiwaCorsOptions): RequestHandler;
|
|
|
56
64
|
*/
|
|
57
65
|
export declare function siwaJsonParser(): RequestHandler;
|
|
58
66
|
/**
|
|
59
|
-
* Express middleware that verifies ERC-8128 HTTP Message Signatures + SIWA receipt
|
|
67
|
+
* Express middleware that verifies ERC-8128 HTTP Message Signatures + SIWA receipt,
|
|
68
|
+
* with optional x402 payment gate.
|
|
69
|
+
*
|
|
70
|
+
* **Without x402** (SIWA only):
|
|
71
|
+
* - Valid SIWA headers → `req.agent` → `next()`
|
|
72
|
+
* - Missing/invalid → 401
|
|
60
73
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
74
|
+
* **With x402** (SIWA + payment):
|
|
75
|
+
* - SIWA must succeed → otherwise 401
|
|
76
|
+
* - Payment must also succeed → otherwise 402 with payment requirements
|
|
77
|
+
* - Both succeed → `req.agent` + `req.payment` → `next()`
|
|
63
78
|
*/
|
|
64
79
|
export declare function siwaMiddleware(options?: SiwaMiddlewareOptions): RequestHandler;
|
|
@@ -16,22 +16,33 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import express from 'express';
|
|
18
18
|
import { verifyAuthenticatedRequest, expressToFetchRequest, resolveReceiptSecret, } from '../erc8128.js';
|
|
19
|
+
import { X402_HEADERS, encodeX402Header, decodeX402Header, processX402Payment, } from '../x402.js';
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// CORS middleware
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
const DEFAULT_SIWA_HEADERS = 'Content-Type, X-SIWA-Receipt, Signature, Signature-Input, Content-Digest';
|
|
24
|
+
const X402_CORS_HEADERS = `${X402_HEADERS.PAYMENT_SIGNATURE}, ${X402_HEADERS.PAYMENT_REQUIRED}`;
|
|
25
|
+
const X402_EXPOSE_HEADERS = `${X402_HEADERS.PAYMENT_REQUIRED}, ${X402_HEADERS.PAYMENT_RESPONSE}`;
|
|
23
26
|
/**
|
|
24
27
|
* CORS middleware pre-configured with SIWA-specific headers.
|
|
25
28
|
* Handles OPTIONS preflight automatically.
|
|
29
|
+
*
|
|
30
|
+
* When `x402: true` is set, also includes x402 payment headers.
|
|
26
31
|
*/
|
|
27
32
|
export function siwaCors(options) {
|
|
28
33
|
const origin = options?.origin ?? '*';
|
|
29
34
|
const methods = options?.methods ?? 'GET, POST, OPTIONS';
|
|
30
|
-
|
|
35
|
+
let headers = options?.headers ?? DEFAULT_SIWA_HEADERS;
|
|
36
|
+
if (options?.x402) {
|
|
37
|
+
headers = `${headers}, ${X402_CORS_HEADERS}`;
|
|
38
|
+
}
|
|
31
39
|
return (req, res, next) => {
|
|
32
40
|
res.header('Access-Control-Allow-Origin', origin);
|
|
33
41
|
res.header('Access-Control-Allow-Methods', methods);
|
|
34
42
|
res.header('Access-Control-Allow-Headers', headers);
|
|
43
|
+
if (options?.x402) {
|
|
44
|
+
res.header('Access-Control-Expose-Headers', X402_EXPOSE_HEADERS);
|
|
45
|
+
}
|
|
35
46
|
if (req.method === 'OPTIONS') {
|
|
36
47
|
res.sendStatus(204);
|
|
37
48
|
return;
|
|
@@ -56,15 +67,25 @@ export function siwaJsonParser() {
|
|
|
56
67
|
// Auth middleware
|
|
57
68
|
// ---------------------------------------------------------------------------
|
|
58
69
|
/**
|
|
59
|
-
* Express middleware that verifies ERC-8128 HTTP Message Signatures + SIWA receipt
|
|
70
|
+
* Express middleware that verifies ERC-8128 HTTP Message Signatures + SIWA receipt,
|
|
71
|
+
* with optional x402 payment gate.
|
|
72
|
+
*
|
|
73
|
+
* **Without x402** (SIWA only):
|
|
74
|
+
* - Valid SIWA headers → `req.agent` → `next()`
|
|
75
|
+
* - Missing/invalid → 401
|
|
60
76
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
77
|
+
* **With x402** (SIWA + payment):
|
|
78
|
+
* - SIWA must succeed → otherwise 401
|
|
79
|
+
* - Payment must also succeed → otherwise 402 with payment requirements
|
|
80
|
+
* - Both succeed → `req.agent` + `req.payment` → `next()`
|
|
63
81
|
*/
|
|
64
82
|
export function siwaMiddleware(options) {
|
|
65
83
|
return async (req, res, next) => {
|
|
66
|
-
|
|
67
|
-
|
|
84
|
+
// -----------------------------------------------------------------------
|
|
85
|
+
// Step 1: SIWA ERC-8128 authentication (always required)
|
|
86
|
+
// -----------------------------------------------------------------------
|
|
87
|
+
const hasSiwaHeaders = req.headers['signature'] && req.headers['x-siwa-receipt'];
|
|
88
|
+
if (!hasSiwaHeaders) {
|
|
68
89
|
res.status(401).json({
|
|
69
90
|
error: 'Unauthorized — provide ERC-8128 Signature + X-SIWA-Receipt headers',
|
|
70
91
|
});
|
|
@@ -85,10 +106,64 @@ export function siwaMiddleware(options) {
|
|
|
85
106
|
return;
|
|
86
107
|
}
|
|
87
108
|
req.agent = result.agent;
|
|
88
|
-
next();
|
|
89
109
|
}
|
|
90
110
|
catch (err) {
|
|
91
111
|
res.status(401).json({ error: `ERC-8128 auth failed: ${err.message}` });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
// Step 2: x402 payment (required only when x402 is configured)
|
|
116
|
+
// -----------------------------------------------------------------------
|
|
117
|
+
if (options?.x402) {
|
|
118
|
+
const { x402 } = options;
|
|
119
|
+
const agentAddress = req.agent.address.toLowerCase();
|
|
120
|
+
// Step 2a: Check session store (SIWX pay-once mode)
|
|
121
|
+
if (x402.session) {
|
|
122
|
+
const existing = await x402.session.store.get(agentAddress, x402.resource.url);
|
|
123
|
+
if (existing) {
|
|
124
|
+
// Active session — skip payment entirely
|
|
125
|
+
next();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const paymentHeader = req.headers[X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase()];
|
|
130
|
+
if (!paymentHeader) {
|
|
131
|
+
// No payment header — return 402 with requirements
|
|
132
|
+
const paymentRequired = {
|
|
133
|
+
accepts: x402.accepts,
|
|
134
|
+
resource: x402.resource,
|
|
135
|
+
};
|
|
136
|
+
res.setHeader(X402_HEADERS.PAYMENT_REQUIRED, encodeX402Header(paymentRequired));
|
|
137
|
+
res.status(402).json({
|
|
138
|
+
error: 'Payment required',
|
|
139
|
+
accepts: x402.accepts,
|
|
140
|
+
resource: x402.resource,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const payload = decodeX402Header(paymentHeader);
|
|
146
|
+
const result = await processX402Payment(payload, x402.accepts, x402.facilitator);
|
|
147
|
+
if (!result.valid) {
|
|
148
|
+
res.status(402).json({ error: result.error });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const responseHeader = encodeX402Header(result.payment);
|
|
152
|
+
res.setHeader(X402_HEADERS.PAYMENT_RESPONSE, responseHeader);
|
|
153
|
+
req.payment = result.payment;
|
|
154
|
+
// Step 2b: Store session after successful payment (SIWX)
|
|
155
|
+
if (x402.session) {
|
|
156
|
+
await x402.session.store.set(agentAddress, x402.resource.url, { paidAt: Date.now(), txHash: result.payment.txHash }, x402.session.ttl);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
res.status(402).json({ error: `x402 payment processing failed: ${err.message}` });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
92
163
|
}
|
|
164
|
+
// -----------------------------------------------------------------------
|
|
165
|
+
// Step 3: All checks passed
|
|
166
|
+
// -----------------------------------------------------------------------
|
|
167
|
+
next();
|
|
93
168
|
};
|
|
94
169
|
}
|
|
@@ -20,10 +20,12 @@
|
|
|
20
20
|
import type { FastifyPluginAsync, preHandlerHookHandler } from 'fastify';
|
|
21
21
|
import { type SiwaAgent, type VerifyOptions } from '../erc8128.js';
|
|
22
22
|
import type { SignerType } from '../signer/index.js';
|
|
23
|
-
|
|
23
|
+
import { type X402Config, type X402Payment } from '../x402.js';
|
|
24
|
+
export type { SiwaAgent, X402Payment };
|
|
24
25
|
declare module 'fastify' {
|
|
25
26
|
interface FastifyRequest {
|
|
26
27
|
agent?: SiwaAgent;
|
|
28
|
+
payment?: X402Payment;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
31
|
export interface SiwaAuthOptions {
|
|
@@ -37,12 +39,16 @@ export interface SiwaAuthOptions {
|
|
|
37
39
|
publicClient?: VerifyOptions['publicClient'];
|
|
38
40
|
/** Allowed signer types. Omit to accept all. */
|
|
39
41
|
allowedSignerTypes?: SignerType[];
|
|
42
|
+
/** Optional x402 payment gate. When set, both SIWA auth AND a valid payment are required. */
|
|
43
|
+
x402?: X402Config;
|
|
40
44
|
}
|
|
41
45
|
export interface SiwaPluginOptions {
|
|
42
46
|
/** CORS allowed origin(s). Defaults to true (reflect origin). */
|
|
43
47
|
origin?: boolean | string | string[];
|
|
44
48
|
/** Allowed headers including SIWA-specific ones. */
|
|
45
49
|
allowedHeaders?: string[];
|
|
50
|
+
/** Include x402 payment headers in CORS. */
|
|
51
|
+
x402?: boolean;
|
|
46
52
|
}
|
|
47
53
|
/**
|
|
48
54
|
* Fastify plugin that sets up CORS with SIWA-specific headers.
|
|
@@ -53,6 +59,7 @@ export declare const siwaPlugin: FastifyPluginAsync<SiwaPluginOptions>;
|
|
|
53
59
|
* Fastify preHandler that verifies ERC-8128 HTTP Message Signatures + SIWA receipt.
|
|
54
60
|
*
|
|
55
61
|
* On success, sets `req.agent` with the verified agent identity.
|
|
56
|
-
*
|
|
62
|
+
* When x402 is configured and payment succeeds, also sets `req.payment`.
|
|
63
|
+
* On failure, responds with 401 (auth) or 402 (payment).
|
|
57
64
|
*/
|
|
58
65
|
export declare function siwaAuth(options?: SiwaAuthOptions): preHandlerHookHandler;
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* ```
|
|
19
19
|
*/
|
|
20
20
|
import { verifyAuthenticatedRequest, resolveReceiptSecret, } from '../erc8128.js';
|
|
21
|
+
import { X402_HEADERS, encodeX402Header, decodeX402Header, processX402Payment, } from '../x402.js';
|
|
21
22
|
// ---------------------------------------------------------------------------
|
|
22
23
|
// CORS headers
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
@@ -28,6 +29,14 @@ const DEFAULT_SIWA_HEADERS = [
|
|
|
28
29
|
'Signature-Input',
|
|
29
30
|
'Content-Digest',
|
|
30
31
|
];
|
|
32
|
+
const X402_CORS_ALLOW = [
|
|
33
|
+
X402_HEADERS.PAYMENT_SIGNATURE,
|
|
34
|
+
X402_HEADERS.PAYMENT_REQUIRED,
|
|
35
|
+
];
|
|
36
|
+
const X402_EXPOSE = [
|
|
37
|
+
X402_HEADERS.PAYMENT_REQUIRED,
|
|
38
|
+
X402_HEADERS.PAYMENT_RESPONSE,
|
|
39
|
+
];
|
|
31
40
|
// ---------------------------------------------------------------------------
|
|
32
41
|
// Request conversion helper
|
|
33
42
|
// ---------------------------------------------------------------------------
|
|
@@ -35,7 +44,8 @@ const DEFAULT_SIWA_HEADERS = [
|
|
|
35
44
|
* Convert a Fastify request to a Fetch Request for verification.
|
|
36
45
|
*/
|
|
37
46
|
function toFetchRequest(req) {
|
|
38
|
-
|
|
47
|
+
// Use req.host (includes port) rather than req.hostname (strips port in Fastify v5)
|
|
48
|
+
const url = `${req.protocol}://${req.host}${req.url}`;
|
|
39
49
|
return new Request(url, {
|
|
40
50
|
method: req.method,
|
|
41
51
|
headers: req.headers,
|
|
@@ -52,12 +62,18 @@ function toFetchRequest(req) {
|
|
|
52
62
|
* Requires @fastify/cors to be installed.
|
|
53
63
|
*/
|
|
54
64
|
export const siwaPlugin = async (fastify, options) => {
|
|
65
|
+
let allowedHeaders = options?.allowedHeaders ?? DEFAULT_SIWA_HEADERS;
|
|
66
|
+
if (options?.x402) {
|
|
67
|
+
allowedHeaders = [...allowedHeaders, ...X402_CORS_ALLOW];
|
|
68
|
+
}
|
|
69
|
+
const exposeHeaders = options?.x402 ? X402_EXPOSE : undefined;
|
|
55
70
|
// Try to register @fastify/cors if available
|
|
56
71
|
try {
|
|
57
72
|
const cors = await import('@fastify/cors');
|
|
58
73
|
await fastify.register(cors.default ?? cors, {
|
|
59
74
|
origin: options?.origin ?? true,
|
|
60
|
-
allowedHeaders
|
|
75
|
+
allowedHeaders,
|
|
76
|
+
exposedHeaders: exposeHeaders,
|
|
61
77
|
});
|
|
62
78
|
}
|
|
63
79
|
catch {
|
|
@@ -65,7 +81,10 @@ export const siwaPlugin = async (fastify, options) => {
|
|
|
65
81
|
fastify.addHook('onSend', async (req, reply) => {
|
|
66
82
|
reply.header('Access-Control-Allow-Origin', '*');
|
|
67
83
|
reply.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
68
|
-
reply.header('Access-Control-Allow-Headers',
|
|
84
|
+
reply.header('Access-Control-Allow-Headers', allowedHeaders.join(', '));
|
|
85
|
+
if (exposeHeaders) {
|
|
86
|
+
reply.header('Access-Control-Expose-Headers', exposeHeaders.join(', '));
|
|
87
|
+
}
|
|
69
88
|
});
|
|
70
89
|
// Handle OPTIONS preflight
|
|
71
90
|
fastify.options('*', async (req, reply) => {
|
|
@@ -80,7 +99,8 @@ export const siwaPlugin = async (fastify, options) => {
|
|
|
80
99
|
* Fastify preHandler that verifies ERC-8128 HTTP Message Signatures + SIWA receipt.
|
|
81
100
|
*
|
|
82
101
|
* On success, sets `req.agent` with the verified agent identity.
|
|
83
|
-
*
|
|
102
|
+
* When x402 is configured and payment succeeds, also sets `req.payment`.
|
|
103
|
+
* On failure, responds with 401 (auth) or 402 (payment).
|
|
84
104
|
*/
|
|
85
105
|
export function siwaAuth(options) {
|
|
86
106
|
return async (req, reply) => {
|
|
@@ -107,5 +127,51 @@ export function siwaAuth(options) {
|
|
|
107
127
|
catch (err) {
|
|
108
128
|
return reply.status(401).send({ error: `ERC-8128 auth failed: ${err.message}` });
|
|
109
129
|
}
|
|
130
|
+
// -----------------------------------------------------------------
|
|
131
|
+
// x402 payment gate
|
|
132
|
+
// -----------------------------------------------------------------
|
|
133
|
+
if (options?.x402) {
|
|
134
|
+
const { x402 } = options;
|
|
135
|
+
const agentAddress = req.agent.address.toLowerCase();
|
|
136
|
+
// Session check
|
|
137
|
+
if (x402.session) {
|
|
138
|
+
const existing = await x402.session.store.get(agentAddress, x402.resource.url);
|
|
139
|
+
if (existing) {
|
|
140
|
+
// Active session — skip payment
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Payment header
|
|
145
|
+
const paymentHeader = req.headers[X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase()];
|
|
146
|
+
if (!paymentHeader) {
|
|
147
|
+
const paymentRequired = {
|
|
148
|
+
accepts: x402.accepts,
|
|
149
|
+
resource: x402.resource,
|
|
150
|
+
};
|
|
151
|
+
reply.header(X402_HEADERS.PAYMENT_REQUIRED, encodeX402Header(paymentRequired));
|
|
152
|
+
return reply.status(402).send({
|
|
153
|
+
error: 'Payment required',
|
|
154
|
+
accepts: x402.accepts,
|
|
155
|
+
resource: x402.resource,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Process payment
|
|
159
|
+
try {
|
|
160
|
+
const payload = decodeX402Header(paymentHeader);
|
|
161
|
+
const payResult = await processX402Payment(payload, x402.accepts, x402.facilitator);
|
|
162
|
+
if (!payResult.valid) {
|
|
163
|
+
return reply.status(402).send({ error: payResult.error });
|
|
164
|
+
}
|
|
165
|
+
reply.header(X402_HEADERS.PAYMENT_RESPONSE, encodeX402Header(payResult.payment));
|
|
166
|
+
req.payment = payResult.payment;
|
|
167
|
+
// Store session after successful payment
|
|
168
|
+
if (x402.session) {
|
|
169
|
+
await x402.session.store.set(agentAddress, x402.resource.url, { paidAt: Date.now(), txHash: payResult.payment.txHash }, x402.session.ttl);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
return reply.status(402).send({ error: `x402 payment processing failed: ${err.message}` });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
110
176
|
};
|
|
111
177
|
}
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
import type { MiddlewareHandler } from 'hono';
|
|
20
20
|
import { type SiwaAgent, type VerifyOptions } from '../erc8128.js';
|
|
21
21
|
import type { SignerType } from '../signer/index.js';
|
|
22
|
-
|
|
22
|
+
import { type X402Config, type X402Payment } from '../x402.js';
|
|
23
|
+
export type { SiwaAgent, X402Payment };
|
|
23
24
|
export interface SiwaMiddlewareOptions {
|
|
24
25
|
/** HMAC secret for receipt verification. Defaults to RECEIPT_SECRET or SIWA_SECRET env. */
|
|
25
26
|
receiptSecret?: string;
|
|
@@ -31,6 +32,8 @@ export interface SiwaMiddlewareOptions {
|
|
|
31
32
|
publicClient?: VerifyOptions['publicClient'];
|
|
32
33
|
/** Allowed signer types. Omit to accept all. */
|
|
33
34
|
allowedSignerTypes?: SignerType[];
|
|
35
|
+
/** Optional x402 payment gate. When set, both SIWA auth AND a valid payment are required. */
|
|
36
|
+
x402?: X402Config;
|
|
34
37
|
}
|
|
35
38
|
export interface SiwaCorsOptions {
|
|
36
39
|
/** Allowed origin(s). Defaults to "*". */
|
|
@@ -39,6 +42,8 @@ export interface SiwaCorsOptions {
|
|
|
39
42
|
methods?: string[];
|
|
40
43
|
/** Allowed headers. */
|
|
41
44
|
headers?: string[];
|
|
45
|
+
/** Include x402 payment headers in CORS. */
|
|
46
|
+
x402?: boolean;
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
49
|
* CORS middleware pre-configured with SIWA-specific headers.
|
|
@@ -49,6 +54,7 @@ export declare function siwaCors(options?: SiwaCorsOptions): MiddlewareHandler;
|
|
|
49
54
|
* Hono middleware that verifies ERC-8128 HTTP Message Signatures + SIWA receipt.
|
|
50
55
|
*
|
|
51
56
|
* On success, sets `c.set("agent", agent)` with the verified agent identity.
|
|
52
|
-
*
|
|
57
|
+
* When x402 is configured and payment succeeds, also sets `c.set("payment", payment)`.
|
|
58
|
+
* On failure, responds with 401 (auth) or 402 (payment).
|
|
53
59
|
*/
|
|
54
60
|
export declare function siwaMiddleware(options?: SiwaMiddlewareOptions): MiddlewareHandler;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
import { verifyAuthenticatedRequest, resolveReceiptSecret, } from '../erc8128.js';
|
|
20
|
+
import { X402_HEADERS, encodeX402Header, decodeX402Header, processX402Payment, } from '../x402.js';
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// CORS middleware
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
@@ -27,6 +28,14 @@ const DEFAULT_SIWA_HEADERS = [
|
|
|
27
28
|
'Signature-Input',
|
|
28
29
|
'Content-Digest',
|
|
29
30
|
];
|
|
31
|
+
const X402_CORS_ALLOW = [
|
|
32
|
+
X402_HEADERS.PAYMENT_SIGNATURE,
|
|
33
|
+
X402_HEADERS.PAYMENT_REQUIRED,
|
|
34
|
+
];
|
|
35
|
+
const X402_EXPOSE = [
|
|
36
|
+
X402_HEADERS.PAYMENT_REQUIRED,
|
|
37
|
+
X402_HEADERS.PAYMENT_RESPONSE,
|
|
38
|
+
];
|
|
30
39
|
/**
|
|
31
40
|
* CORS middleware pre-configured with SIWA-specific headers.
|
|
32
41
|
* Handles OPTIONS preflight automatically.
|
|
@@ -34,11 +43,17 @@ const DEFAULT_SIWA_HEADERS = [
|
|
|
34
43
|
export function siwaCors(options) {
|
|
35
44
|
const origin = options?.origin ?? '*';
|
|
36
45
|
const methods = options?.methods ?? ['GET', 'POST', 'OPTIONS'];
|
|
37
|
-
|
|
46
|
+
let headers = options?.headers ?? DEFAULT_SIWA_HEADERS;
|
|
47
|
+
if (options?.x402) {
|
|
48
|
+
headers = [...headers, ...X402_CORS_ALLOW];
|
|
49
|
+
}
|
|
38
50
|
return async (c, next) => {
|
|
39
51
|
c.header('Access-Control-Allow-Origin', origin);
|
|
40
52
|
c.header('Access-Control-Allow-Methods', methods.join(', '));
|
|
41
53
|
c.header('Access-Control-Allow-Headers', headers.join(', '));
|
|
54
|
+
if (options?.x402) {
|
|
55
|
+
c.header('Access-Control-Expose-Headers', X402_EXPOSE.join(', '));
|
|
56
|
+
}
|
|
42
57
|
if (c.req.method === 'OPTIONS') {
|
|
43
58
|
return c.body(null, 204);
|
|
44
59
|
}
|
|
@@ -52,7 +67,8 @@ export function siwaCors(options) {
|
|
|
52
67
|
* Hono middleware that verifies ERC-8128 HTTP Message Signatures + SIWA receipt.
|
|
53
68
|
*
|
|
54
69
|
* On success, sets `c.set("agent", agent)` with the verified agent identity.
|
|
55
|
-
*
|
|
70
|
+
* When x402 is configured and payment succeeds, also sets `c.set("payment", payment)`.
|
|
71
|
+
* On failure, responds with 401 (auth) or 402 (payment).
|
|
56
72
|
*/
|
|
57
73
|
export function siwaMiddleware(options) {
|
|
58
74
|
return async (c, next) => {
|
|
@@ -60,6 +76,7 @@ export function siwaMiddleware(options) {
|
|
|
60
76
|
if (!hasSignature) {
|
|
61
77
|
return c.json({ error: 'Unauthorized — provide ERC-8128 Signature + X-SIWA-Receipt headers' }, 401);
|
|
62
78
|
}
|
|
79
|
+
let agent;
|
|
63
80
|
try {
|
|
64
81
|
const secret = resolveReceiptSecret(options?.receiptSecret);
|
|
65
82
|
const result = await verifyAuthenticatedRequest(c.req.raw, {
|
|
@@ -72,11 +89,56 @@ export function siwaMiddleware(options) {
|
|
|
72
89
|
if (!result.valid) {
|
|
73
90
|
return c.json({ error: result.error }, 401);
|
|
74
91
|
}
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
agent = result.agent;
|
|
93
|
+
c.set('agent', agent);
|
|
77
94
|
}
|
|
78
95
|
catch (err) {
|
|
79
96
|
return c.json({ error: `ERC-8128 auth failed: ${err.message}` }, 401);
|
|
80
97
|
}
|
|
98
|
+
// -----------------------------------------------------------------
|
|
99
|
+
// x402 payment gate
|
|
100
|
+
// -----------------------------------------------------------------
|
|
101
|
+
if (options?.x402) {
|
|
102
|
+
const { x402 } = options;
|
|
103
|
+
const agentAddress = agent.address.toLowerCase();
|
|
104
|
+
// Session check
|
|
105
|
+
if (x402.session) {
|
|
106
|
+
const existing = await x402.session.store.get(agentAddress, x402.resource.url);
|
|
107
|
+
if (existing) {
|
|
108
|
+
// Active session — skip payment
|
|
109
|
+
await next();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Payment header
|
|
114
|
+
const paymentHeader = c.req.header(X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase())
|
|
115
|
+
?? c.req.header(X402_HEADERS.PAYMENT_SIGNATURE);
|
|
116
|
+
if (!paymentHeader) {
|
|
117
|
+
const paymentRequired = {
|
|
118
|
+
accepts: x402.accepts,
|
|
119
|
+
resource: x402.resource,
|
|
120
|
+
};
|
|
121
|
+
c.header(X402_HEADERS.PAYMENT_REQUIRED, encodeX402Header(paymentRequired));
|
|
122
|
+
return c.json({ error: 'Payment required', accepts: x402.accepts, resource: x402.resource }, 402);
|
|
123
|
+
}
|
|
124
|
+
// Process payment
|
|
125
|
+
try {
|
|
126
|
+
const payload = decodeX402Header(paymentHeader);
|
|
127
|
+
const payResult = await processX402Payment(payload, x402.accepts, x402.facilitator);
|
|
128
|
+
if (!payResult.valid) {
|
|
129
|
+
return c.json({ error: payResult.error }, 402);
|
|
130
|
+
}
|
|
131
|
+
c.header(X402_HEADERS.PAYMENT_RESPONSE, encodeX402Header(payResult.payment));
|
|
132
|
+
c.set('payment', payResult.payment);
|
|
133
|
+
// Store session after successful payment
|
|
134
|
+
if (x402.session) {
|
|
135
|
+
await x402.session.store.set(agentAddress, x402.resource.url, { paidAt: Date.now(), txHash: payResult.payment.txHash }, x402.session.ttl);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return c.json({ error: `x402 payment processing failed: ${err.message}` }, 402);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await next();
|
|
81
143
|
};
|
|
82
144
|
}
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type SiwaAgent } from '../erc8128.js';
|
|
20
20
|
import type { SignerType } from '../signer/index.js';
|
|
21
|
-
|
|
21
|
+
import { type X402Config, type X402Payment } from '../x402.js';
|
|
22
|
+
export type { SiwaAgent, X402Payment };
|
|
22
23
|
export interface WithSiwaOptions {
|
|
23
24
|
/** HMAC secret for receipt verification. Defaults to RECEIPT_SECRET or SIWA_SECRET env. */
|
|
24
25
|
receiptSecret?: string;
|
|
@@ -28,16 +29,23 @@ export interface WithSiwaOptions {
|
|
|
28
29
|
verifyOnchain?: boolean;
|
|
29
30
|
/** Allowed signer types. Omit to accept all. */
|
|
30
31
|
allowedSignerTypes?: SignerType[];
|
|
32
|
+
/** Optional x402 payment gate. When set, both SIWA auth AND a valid payment are required. */
|
|
33
|
+
x402?: X402Config;
|
|
34
|
+
}
|
|
35
|
+
export interface CorsOptions {
|
|
36
|
+
/** Include x402 payment headers in CORS. */
|
|
37
|
+
x402?: boolean;
|
|
31
38
|
}
|
|
32
39
|
/** CORS headers required by SIWA-authenticated requests. */
|
|
33
|
-
export declare function corsHeaders(): Record<string, string>;
|
|
40
|
+
export declare function corsHeaders(options?: CorsOptions): Record<string, string>;
|
|
34
41
|
/** Return a JSON Response with CORS headers. */
|
|
35
42
|
export declare function corsJson(data: unknown, init?: {
|
|
36
43
|
status?: number;
|
|
37
|
-
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
}, corsOpts?: CorsOptions): Response;
|
|
38
46
|
/** Return a 204 OPTIONS response with CORS headers. */
|
|
39
|
-
export declare function siwaOptions(): Response;
|
|
40
|
-
type SiwaHandler = (agent: SiwaAgent, req: Request) => Promise<Record<string, unknown> | Response> | Record<string, unknown> | Response;
|
|
47
|
+
export declare function siwaOptions(corsOpts?: CorsOptions): Response;
|
|
48
|
+
type SiwaHandler = (agent: SiwaAgent, req: Request, payment?: X402Payment) => Promise<Record<string, unknown> | Response> | Record<string, unknown> | Response;
|
|
41
49
|
/**
|
|
42
50
|
* Wrap a Next.js route handler with SIWA ERC-8128 authentication.
|
|
43
51
|
*
|
|
@@ -45,5 +53,6 @@ type SiwaHandler = (agent: SiwaAgent, req: Request) => Promise<Record<string, un
|
|
|
45
53
|
* - Normalizes the request URL for reverse-proxy environments
|
|
46
54
|
* - Returns 401 with CORS headers on auth failure
|
|
47
55
|
* - If the handler returns a plain object, it is auto-wrapped in a JSON Response with CORS headers
|
|
56
|
+
* - When x402 is configured, requires payment after SIWA auth succeeds
|
|
48
57
|
*/
|
|
49
58
|
export declare function withSiwa(handler: SiwaHandler, options?: WithSiwaOptions): (req: Request) => Promise<Response>;
|
|
@@ -17,30 +17,43 @@
|
|
|
17
17
|
* ```
|
|
18
18
|
*/
|
|
19
19
|
import { verifyAuthenticatedRequest, nextjsToFetchRequest, resolveReceiptSecret, } from '../erc8128.js';
|
|
20
|
+
import { X402_HEADERS, encodeX402Header, decodeX402Header, processX402Payment, } from '../x402.js';
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// CORS helpers
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
24
|
+
const DEFAULT_SIWA_HEADERS = 'Content-Type, X-SIWA-Receipt, Signature, Signature-Input, Content-Digest';
|
|
25
|
+
const X402_CORS_HEADERS = `${X402_HEADERS.PAYMENT_SIGNATURE}, ${X402_HEADERS.PAYMENT_REQUIRED}`;
|
|
26
|
+
const X402_EXPOSE_HEADERS = `${X402_HEADERS.PAYMENT_REQUIRED}, ${X402_HEADERS.PAYMENT_RESPONSE}`;
|
|
23
27
|
/** CORS headers required by SIWA-authenticated requests. */
|
|
24
|
-
export function corsHeaders() {
|
|
25
|
-
|
|
28
|
+
export function corsHeaders(options) {
|
|
29
|
+
let allowHeaders = DEFAULT_SIWA_HEADERS;
|
|
30
|
+
if (options?.x402) {
|
|
31
|
+
allowHeaders = `${allowHeaders}, ${X402_CORS_HEADERS}`;
|
|
32
|
+
}
|
|
33
|
+
const headers = {
|
|
26
34
|
'Access-Control-Allow-Origin': '*',
|
|
27
35
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
28
|
-
'Access-Control-Allow-Headers':
|
|
36
|
+
'Access-Control-Allow-Headers': allowHeaders,
|
|
29
37
|
};
|
|
38
|
+
if (options?.x402) {
|
|
39
|
+
headers['Access-Control-Expose-Headers'] = X402_EXPOSE_HEADERS;
|
|
40
|
+
}
|
|
41
|
+
return headers;
|
|
30
42
|
}
|
|
31
43
|
/** Return a JSON Response with CORS headers. */
|
|
32
|
-
export function corsJson(data, init) {
|
|
44
|
+
export function corsJson(data, init, corsOpts) {
|
|
33
45
|
return new Response(JSON.stringify(data), {
|
|
34
46
|
status: init?.status ?? 200,
|
|
35
47
|
headers: {
|
|
36
48
|
'Content-Type': 'application/json',
|
|
37
|
-
...corsHeaders(),
|
|
49
|
+
...corsHeaders(corsOpts),
|
|
50
|
+
...init?.headers,
|
|
38
51
|
},
|
|
39
52
|
});
|
|
40
53
|
}
|
|
41
54
|
/** Return a 204 OPTIONS response with CORS headers. */
|
|
42
|
-
export function siwaOptions() {
|
|
43
|
-
return new Response(null, { status: 204, headers: corsHeaders() });
|
|
55
|
+
export function siwaOptions(corsOpts) {
|
|
56
|
+
return new Response(null, { status: 204, headers: corsHeaders(corsOpts) });
|
|
44
57
|
}
|
|
45
58
|
/**
|
|
46
59
|
* Wrap a Next.js route handler with SIWA ERC-8128 authentication.
|
|
@@ -49,8 +62,10 @@ export function siwaOptions() {
|
|
|
49
62
|
* - Normalizes the request URL for reverse-proxy environments
|
|
50
63
|
* - Returns 401 with CORS headers on auth failure
|
|
51
64
|
* - If the handler returns a plain object, it is auto-wrapped in a JSON Response with CORS headers
|
|
65
|
+
* - When x402 is configured, requires payment after SIWA auth succeeds
|
|
52
66
|
*/
|
|
53
67
|
export function withSiwa(handler, options) {
|
|
68
|
+
const corsOpts = options?.x402 ? { x402: true } : undefined;
|
|
54
69
|
return async (req) => {
|
|
55
70
|
const secret = resolveReceiptSecret(options?.receiptSecret);
|
|
56
71
|
// Clone for body-consuming methods so handler can still read the body
|
|
@@ -64,21 +79,86 @@ export function withSiwa(handler, options) {
|
|
|
64
79
|
};
|
|
65
80
|
const result = await verifyAuthenticatedRequest(nextjsToFetchRequest(verifyReq), verifyOptions);
|
|
66
81
|
if (!result.valid) {
|
|
67
|
-
return corsJson({ error: result.error }, { status: 401 });
|
|
82
|
+
return corsJson({ error: result.error }, { status: 401 }, corsOpts);
|
|
68
83
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
// -----------------------------------------------------------------
|
|
85
|
+
// x402 payment gate
|
|
86
|
+
// -----------------------------------------------------------------
|
|
87
|
+
let payment;
|
|
88
|
+
if (options?.x402) {
|
|
89
|
+
const { x402 } = options;
|
|
90
|
+
const agentAddress = result.agent.address.toLowerCase();
|
|
91
|
+
// Session check
|
|
92
|
+
if (x402.session) {
|
|
93
|
+
const existing = await x402.session.store.get(agentAddress, x402.resource.url);
|
|
94
|
+
if (existing) {
|
|
95
|
+
// Active session — skip payment, call handler without payment arg
|
|
96
|
+
const response = await handler(result.agent, req);
|
|
97
|
+
return wrapResponse(response, corsOpts);
|
|
98
|
+
}
|
|
75
99
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
// Payment header
|
|
101
|
+
const paymentHeader = req.headers.get(X402_HEADERS.PAYMENT_SIGNATURE.toLowerCase())
|
|
102
|
+
?? req.headers.get(X402_HEADERS.PAYMENT_SIGNATURE);
|
|
103
|
+
if (!paymentHeader) {
|
|
104
|
+
const paymentRequired = {
|
|
105
|
+
accepts: x402.accepts,
|
|
106
|
+
resource: x402.resource,
|
|
107
|
+
};
|
|
108
|
+
return corsJson({ error: 'Payment required', accepts: x402.accepts, resource: x402.resource }, {
|
|
109
|
+
status: 402,
|
|
110
|
+
headers: { [X402_HEADERS.PAYMENT_REQUIRED]: encodeX402Header(paymentRequired) },
|
|
111
|
+
}, corsOpts);
|
|
112
|
+
}
|
|
113
|
+
// Process payment
|
|
114
|
+
try {
|
|
115
|
+
const payload = decodeX402Header(paymentHeader);
|
|
116
|
+
const payResult = await processX402Payment(payload, x402.accepts, x402.facilitator);
|
|
117
|
+
if (!payResult.valid) {
|
|
118
|
+
return corsJson({ error: payResult.error }, { status: 402 }, corsOpts);
|
|
119
|
+
}
|
|
120
|
+
payment = payResult.payment;
|
|
121
|
+
// Store session after successful payment
|
|
122
|
+
if (x402.session) {
|
|
123
|
+
await x402.session.store.set(agentAddress, x402.resource.url, { paidAt: Date.now(), txHash: payResult.payment.txHash }, x402.session.ttl);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
return corsJson({ error: `x402 payment processing failed: ${err.message}` }, { status: 402 }, corsOpts);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// -----------------------------------------------------------------
|
|
131
|
+
// Call handler
|
|
132
|
+
// -----------------------------------------------------------------
|
|
133
|
+
const response = await handler(result.agent, req, payment);
|
|
134
|
+
const wrapped = wrapResponse(response, corsOpts);
|
|
135
|
+
// Add Payment-Response header if payment was processed
|
|
136
|
+
if (payment) {
|
|
137
|
+
const headers = new Headers(wrapped.headers);
|
|
138
|
+
headers.set(X402_HEADERS.PAYMENT_RESPONSE, encodeX402Header(payment));
|
|
139
|
+
return new Response(wrapped.body, {
|
|
140
|
+
status: wrapped.status,
|
|
141
|
+
statusText: wrapped.statusText,
|
|
79
142
|
headers,
|
|
80
143
|
});
|
|
81
144
|
}
|
|
82
|
-
return
|
|
145
|
+
return wrapped;
|
|
83
146
|
};
|
|
84
147
|
}
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Internal helpers
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
function wrapResponse(response, corsOpts) {
|
|
152
|
+
if (response instanceof Response) {
|
|
153
|
+
const headers = new Headers(response.headers);
|
|
154
|
+
for (const [k, v] of Object.entries(corsHeaders(corsOpts))) {
|
|
155
|
+
headers.set(k, v);
|
|
156
|
+
}
|
|
157
|
+
return new Response(response.body, {
|
|
158
|
+
status: response.status,
|
|
159
|
+
statusText: response.statusText,
|
|
160
|
+
headers,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return corsJson(response, undefined, corsOpts);
|
|
164
|
+
}
|
package/dist/x402.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402.ts
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic x402 payment protocol integration.
|
|
5
|
+
*
|
|
6
|
+
* Provides types, header constants, base64 encode/decode helpers,
|
|
7
|
+
* a facilitator HTTP client, and a high-level processX402Payment function.
|
|
8
|
+
*
|
|
9
|
+
* No framework imports. No `@x402/*` dependency — the facilitator API
|
|
10
|
+
* is just 2 HTTP POSTs (verify + settle).
|
|
11
|
+
*/
|
|
12
|
+
export declare const X402_HEADERS: {
|
|
13
|
+
readonly PAYMENT_REQUIRED: "Payment-Required";
|
|
14
|
+
readonly PAYMENT_SIGNATURE: "Payment-Signature";
|
|
15
|
+
readonly PAYMENT_RESPONSE: "Payment-Response";
|
|
16
|
+
};
|
|
17
|
+
/** Network identifier (e.g. "eip155:84532") */
|
|
18
|
+
export type Network = string;
|
|
19
|
+
/** Resource being paid for */
|
|
20
|
+
export interface ResourceInfo {
|
|
21
|
+
url: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}
|
|
24
|
+
/** A single payment option the server accepts */
|
|
25
|
+
export interface PaymentRequirements {
|
|
26
|
+
scheme: string;
|
|
27
|
+
network: Network;
|
|
28
|
+
amount: string;
|
|
29
|
+
asset: string;
|
|
30
|
+
payTo: string;
|
|
31
|
+
maxTimeoutSeconds?: number;
|
|
32
|
+
}
|
|
33
|
+
/** Full 402 response payload (base64-encoded in header) */
|
|
34
|
+
export interface PaymentRequired {
|
|
35
|
+
accepts: PaymentRequirements[];
|
|
36
|
+
resource: ResourceInfo;
|
|
37
|
+
}
|
|
38
|
+
/** Client-side payment payload (base64-encoded in header) */
|
|
39
|
+
export interface PaymentPayload {
|
|
40
|
+
signature: string;
|
|
41
|
+
payment: {
|
|
42
|
+
scheme: string;
|
|
43
|
+
network: Network;
|
|
44
|
+
amount: string;
|
|
45
|
+
asset: string;
|
|
46
|
+
payTo: string;
|
|
47
|
+
nonce?: string;
|
|
48
|
+
};
|
|
49
|
+
resource: ResourceInfo;
|
|
50
|
+
}
|
|
51
|
+
/** Facilitator verify response */
|
|
52
|
+
export interface VerifyResponse {
|
|
53
|
+
valid: boolean;
|
|
54
|
+
reason?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Facilitator settle response */
|
|
57
|
+
export interface SettleResponse {
|
|
58
|
+
success: boolean;
|
|
59
|
+
txHash?: string;
|
|
60
|
+
reason?: string;
|
|
61
|
+
}
|
|
62
|
+
/** Result of processing an x402 payment */
|
|
63
|
+
export type X402Result = {
|
|
64
|
+
valid: true;
|
|
65
|
+
payment: X402Payment;
|
|
66
|
+
} | {
|
|
67
|
+
valid: false;
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
/** Verified payment info attached to the request */
|
|
71
|
+
export interface X402Payment {
|
|
72
|
+
scheme: string;
|
|
73
|
+
network: Network;
|
|
74
|
+
amount: string;
|
|
75
|
+
asset: string;
|
|
76
|
+
payTo: string;
|
|
77
|
+
txHash?: string;
|
|
78
|
+
}
|
|
79
|
+
/** Facilitator client with verify + settle methods */
|
|
80
|
+
export interface FacilitatorClient {
|
|
81
|
+
verify(payload: PaymentPayload, requirements: PaymentRequirements[]): Promise<VerifyResponse>;
|
|
82
|
+
settle(payload: PaymentPayload, requirements: PaymentRequirements[]): Promise<SettleResponse>;
|
|
83
|
+
}
|
|
84
|
+
/** Stored session data for a paid agent */
|
|
85
|
+
export interface X402Session {
|
|
86
|
+
paidAt: number;
|
|
87
|
+
txHash?: string;
|
|
88
|
+
}
|
|
89
|
+
/** Pluggable session store keyed by (address, resource) */
|
|
90
|
+
export interface X402SessionStore {
|
|
91
|
+
get(address: string, resource: string): Promise<X402Session | null>;
|
|
92
|
+
set(address: string, resource: string, session: X402Session, ttlMs: number): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
/** Session configuration for SIWX pay-once mode */
|
|
95
|
+
export interface X402SessionConfig {
|
|
96
|
+
store: X402SessionStore;
|
|
97
|
+
/** Session TTL in milliseconds */
|
|
98
|
+
ttl: number;
|
|
99
|
+
}
|
|
100
|
+
/** x402 configuration for middleware */
|
|
101
|
+
export interface X402Config {
|
|
102
|
+
facilitator: FacilitatorClient;
|
|
103
|
+
resource: ResourceInfo;
|
|
104
|
+
accepts: PaymentRequirements[];
|
|
105
|
+
/** Enable SIWX pay-once sessions. When set, payment is only required on first request. */
|
|
106
|
+
session?: X402SessionConfig;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Encode data as a base64 JSON string for use in HTTP headers.
|
|
110
|
+
*/
|
|
111
|
+
export declare function encodeX402Header(data: unknown): string;
|
|
112
|
+
/**
|
|
113
|
+
* Decode a base64 JSON header value.
|
|
114
|
+
*/
|
|
115
|
+
export declare function decodeX402Header<T = unknown>(header: string): T;
|
|
116
|
+
/**
|
|
117
|
+
* Create a facilitator client that communicates via HTTP.
|
|
118
|
+
*
|
|
119
|
+
* The x402 facilitator exposes two endpoints:
|
|
120
|
+
* - POST /verify — validates a payment signature
|
|
121
|
+
* - POST /settle — settles the payment on-chain
|
|
122
|
+
*/
|
|
123
|
+
export declare function createFacilitatorClient(options: {
|
|
124
|
+
url: string;
|
|
125
|
+
}): FacilitatorClient;
|
|
126
|
+
/**
|
|
127
|
+
* In-memory X402 session store with TTL-based expiry.
|
|
128
|
+
*
|
|
129
|
+
* Suitable for single-process servers. For multi-instance deployments,
|
|
130
|
+
* implement X402SessionStore with a shared store (Redis, database, etc.).
|
|
131
|
+
*/
|
|
132
|
+
export declare function createMemoryX402SessionStore(): X402SessionStore;
|
|
133
|
+
/**
|
|
134
|
+
* Verify and settle an x402 payment in one call.
|
|
135
|
+
*
|
|
136
|
+
* 1. Calls facilitator.verify() to validate the payment signature
|
|
137
|
+
* 2. If valid, calls facilitator.settle() to execute the on-chain transfer
|
|
138
|
+
* 3. Returns the payment details with transaction hash
|
|
139
|
+
*/
|
|
140
|
+
export declare function processX402Payment(payload: PaymentPayload, accepts: PaymentRequirements[], facilitator: FacilitatorClient): Promise<X402Result>;
|
package/dist/x402.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402.ts
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic x402 payment protocol integration.
|
|
5
|
+
*
|
|
6
|
+
* Provides types, header constants, base64 encode/decode helpers,
|
|
7
|
+
* a facilitator HTTP client, and a high-level processX402Payment function.
|
|
8
|
+
*
|
|
9
|
+
* No framework imports. No `@x402/*` dependency — the facilitator API
|
|
10
|
+
* is just 2 HTTP POSTs (verify + settle).
|
|
11
|
+
*/
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Header constants
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
export const X402_HEADERS = {
|
|
16
|
+
PAYMENT_REQUIRED: 'Payment-Required',
|
|
17
|
+
PAYMENT_SIGNATURE: 'Payment-Signature',
|
|
18
|
+
PAYMENT_RESPONSE: 'Payment-Response',
|
|
19
|
+
};
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Base64 JSON encode / decode
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Encode data as a base64 JSON string for use in HTTP headers.
|
|
25
|
+
*/
|
|
26
|
+
export function encodeX402Header(data) {
|
|
27
|
+
const json = JSON.stringify(data);
|
|
28
|
+
if (typeof Buffer !== 'undefined') {
|
|
29
|
+
return Buffer.from(json).toString('base64');
|
|
30
|
+
}
|
|
31
|
+
return btoa(json);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Decode a base64 JSON header value.
|
|
35
|
+
*/
|
|
36
|
+
export function decodeX402Header(header) {
|
|
37
|
+
let json;
|
|
38
|
+
if (typeof Buffer !== 'undefined') {
|
|
39
|
+
json = Buffer.from(header, 'base64').toString('utf-8');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
json = atob(header);
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(json);
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Facilitator client
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/**
|
|
50
|
+
* Create a facilitator client that communicates via HTTP.
|
|
51
|
+
*
|
|
52
|
+
* The x402 facilitator exposes two endpoints:
|
|
53
|
+
* - POST /verify — validates a payment signature
|
|
54
|
+
* - POST /settle — settles the payment on-chain
|
|
55
|
+
*/
|
|
56
|
+
export function createFacilitatorClient(options) {
|
|
57
|
+
const baseUrl = options.url.replace(/\/+$/, '');
|
|
58
|
+
return {
|
|
59
|
+
async verify(payload, requirements) {
|
|
60
|
+
const res = await fetch(`${baseUrl}/verify`, {
|
|
61
|
+
method: 'POST',
|
|
62
|
+
headers: { 'Content-Type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({ payload, requirements }),
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const text = await res.text().catch(() => res.statusText);
|
|
67
|
+
throw new Error(`Facilitator verify failed (${res.status}): ${text}`);
|
|
68
|
+
}
|
|
69
|
+
return res.json();
|
|
70
|
+
},
|
|
71
|
+
async settle(payload, requirements) {
|
|
72
|
+
const res = await fetch(`${baseUrl}/settle`, {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify({ payload, requirements }),
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const text = await res.text().catch(() => res.statusText);
|
|
79
|
+
throw new Error(`Facilitator settle failed (${res.status}): ${text}`);
|
|
80
|
+
}
|
|
81
|
+
return res.json();
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Session store — in-memory
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
/**
|
|
89
|
+
* In-memory X402 session store with TTL-based expiry.
|
|
90
|
+
*
|
|
91
|
+
* Suitable for single-process servers. For multi-instance deployments,
|
|
92
|
+
* implement X402SessionStore with a shared store (Redis, database, etc.).
|
|
93
|
+
*/
|
|
94
|
+
export function createMemoryX402SessionStore() {
|
|
95
|
+
const sessions = new Map();
|
|
96
|
+
function cleanup() {
|
|
97
|
+
const now = Date.now();
|
|
98
|
+
for (const [k, v] of sessions) {
|
|
99
|
+
if (v.expiry < now)
|
|
100
|
+
sessions.delete(k);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
async get(address, resource) {
|
|
105
|
+
cleanup();
|
|
106
|
+
const key = `${address}:${resource}`;
|
|
107
|
+
const entry = sessions.get(key);
|
|
108
|
+
if (!entry)
|
|
109
|
+
return null;
|
|
110
|
+
if (entry.expiry < Date.now()) {
|
|
111
|
+
sessions.delete(key);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
return entry.session;
|
|
115
|
+
},
|
|
116
|
+
async set(address, resource, session, ttlMs) {
|
|
117
|
+
cleanup();
|
|
118
|
+
const key = `${address}:${resource}`;
|
|
119
|
+
sessions.set(key, { session, expiry: Date.now() + ttlMs });
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Payment processing
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
/**
|
|
127
|
+
* Verify and settle an x402 payment in one call.
|
|
128
|
+
*
|
|
129
|
+
* 1. Calls facilitator.verify() to validate the payment signature
|
|
130
|
+
* 2. If valid, calls facilitator.settle() to execute the on-chain transfer
|
|
131
|
+
* 3. Returns the payment details with transaction hash
|
|
132
|
+
*/
|
|
133
|
+
export async function processX402Payment(payload, accepts, facilitator) {
|
|
134
|
+
// 1. Verify the payment signature
|
|
135
|
+
const verifyResult = await facilitator.verify(payload, accepts);
|
|
136
|
+
if (!verifyResult.valid) {
|
|
137
|
+
return {
|
|
138
|
+
valid: false,
|
|
139
|
+
error: `Payment verification failed: ${verifyResult.reason ?? 'unknown'}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// 2. Settle the payment on-chain
|
|
143
|
+
const settleResult = await facilitator.settle(payload, accepts);
|
|
144
|
+
if (!settleResult.success) {
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
error: `Payment settlement failed: ${settleResult.reason ?? 'unknown'}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
valid: true,
|
|
152
|
+
payment: {
|
|
153
|
+
scheme: payload.payment.scheme,
|
|
154
|
+
network: payload.payment.network,
|
|
155
|
+
amount: payload.payment.amount,
|
|
156
|
+
asset: payload.payment.asset,
|
|
157
|
+
payTo: payload.payment.payTo,
|
|
158
|
+
txHash: settleResult.txHash,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buildersgarden/siwa",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"./tba": {
|
|
67
67
|
"types": "./dist/tba.d.ts",
|
|
68
68
|
"default": "./dist/tba.js"
|
|
69
|
+
},
|
|
70
|
+
"./x402": {
|
|
71
|
+
"types": "./dist/x402.d.ts",
|
|
72
|
+
"default": "./dist/x402.js"
|
|
69
73
|
}
|
|
70
74
|
},
|
|
71
75
|
"main": "./dist/index.js",
|