@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 CHANGED
@@ -9,3 +9,4 @@ export * from './receipt.js';
9
9
  export * from './erc8128.js';
10
10
  export * from './nonce-store.js';
11
11
  export * from './tba.js';
12
+ export * from './x402.js';
package/dist/index.js CHANGED
@@ -9,3 +9,4 @@ export * from './receipt.js';
9
9
  export * from './erc8128.js';
10
10
  export * from './nonce-store.js';
11
11
  export * from './tba.js';
12
+ export * from './x402.js';
@@ -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
- * On success, sets `req.agent` with the verified agent identity.
62
- * On failure, responds with 401.
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
- const headers = options?.headers ?? DEFAULT_SIWA_HEADERS;
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
- * On success, sets `req.agent` with the verified agent identity.
62
- * On failure, responds with 401.
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
- const hasSignature = req.headers['signature'] && req.headers['x-siwa-receipt'];
67
- if (!hasSignature) {
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
- export type { SiwaAgent };
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
- * On failure, responds with 401.
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
- const url = `${req.protocol}://${req.hostname}${req.url}`;
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: options?.allowedHeaders ?? DEFAULT_SIWA_HEADERS,
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', (options?.allowedHeaders ?? DEFAULT_SIWA_HEADERS).join(', '));
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
- * On failure, responds with 401.
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
- export type { SiwaAgent };
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
- * On failure, responds with 401.
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
- const headers = options?.headers ?? DEFAULT_SIWA_HEADERS;
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
- * On failure, responds with 401.
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
- c.set('agent', result.agent);
76
- await next();
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
- export type { SiwaAgent };
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
- }): Response;
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
- return {
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': 'Content-Type, X-SIWA-Receipt, Signature, Signature-Input, Content-Digest',
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
- const response = await handler(result.agent, req);
70
- if (response instanceof Response) {
71
- // Merge CORS headers into existing response
72
- const headers = new Headers(response.headers);
73
- for (const [k, v] of Object.entries(corsHeaders())) {
74
- headers.set(k, v);
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
- return new Response(response.body, {
77
- status: response.status,
78
- statusText: response.statusText,
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 corsJson(response);
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.17",
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",