x402-engineer 0.1.0
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/AGENT.md +102 -0
- package/README.md +43 -0
- package/dist/cli.cjs +137 -0
- package/package.json +51 -0
- package/skills/stellar-dev/SKILL.md +146 -0
- package/skills/stellar-dev/advanced-patterns.md +188 -0
- package/skills/stellar-dev/api-rpc-horizon.md +521 -0
- package/skills/stellar-dev/common-pitfalls.md +510 -0
- package/skills/stellar-dev/contracts-soroban.md +565 -0
- package/skills/stellar-dev/ecosystem.md +430 -0
- package/skills/stellar-dev/frontend-stellar-sdk.md +651 -0
- package/skills/stellar-dev/resources.md +306 -0
- package/skills/stellar-dev/security.md +491 -0
- package/skills/stellar-dev/standards-reference.md +94 -0
- package/skills/stellar-dev/stellar-assets.md +419 -0
- package/skills/stellar-dev/testing.md +786 -0
- package/skills/stellar-dev/zk-proofs.md +136 -0
- package/skills/x402-add-paywall/SKILL.md +208 -0
- package/skills/x402-add-paywall/references/patterns.md +132 -0
- package/skills/x402-debug/SKILL.md +92 -0
- package/skills/x402-debug/references/checklist.md +146 -0
- package/skills/x402-explain/SKILL.md +136 -0
- package/skills/x402-init/SKILL.md +129 -0
- package/skills/x402-init/templates/env-example.md +17 -0
- package/skills/x402-init/templates/express/config.ts.md +29 -0
- package/skills/x402-init/templates/express/server.ts.md +30 -0
- package/skills/x402-init/templates/fastify/adapter.ts.md +66 -0
- package/skills/x402-init/templates/fastify/config.ts.md +29 -0
- package/skills/x402-init/templates/fastify/server.ts.md +90 -0
- package/skills/x402-init/templates/hono/config.ts.md +29 -0
- package/skills/x402-init/templates/hono/server.ts.md +31 -0
- package/skills/x402-init/templates/next-app-router/config.ts.md +29 -0
- package/skills/x402-init/templates/next-app-router/server.ts.md +31 -0
- package/skills/x402-stellar/SKILL.md +139 -0
- package/skills/x402-stellar/references/api.md +237 -0
- package/skills/x402-stellar/references/patterns.md +276 -0
- package/skills/x402-stellar/references/setup.md +138 -0
- package/skills/x402-stellar/scripts/check-deps.js +218 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# x402-stellar Code Patterns
|
|
2
|
+
|
|
3
|
+
These are **working, production-tested** patterns extracted from a deployed x402 POC. Use these exact patterns — do not improvise the import paths or class usage.
|
|
4
|
+
|
|
5
|
+
## Server Pattern (Express + x402 Middleware)
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import express from "express";
|
|
9
|
+
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
|
|
10
|
+
import { ExactStellarScheme } from "@x402/stellar/exact/server";
|
|
11
|
+
import { HTTPFacilitatorClient } from "@x402/core/server";
|
|
12
|
+
import "dotenv/config";
|
|
13
|
+
|
|
14
|
+
// 1. Create the facilitator client with auth headers
|
|
15
|
+
const facilitatorClient = new HTTPFacilitatorClient({
|
|
16
|
+
url: process.env.FACILITATOR_URL || "https://channels.openzeppelin.com/x402/testnet",
|
|
17
|
+
createAuthHeaders: async () => {
|
|
18
|
+
const headers = { Authorization: `Bearer ${process.env.FACILITATOR_API_KEY}` };
|
|
19
|
+
return { verify: headers, settle: headers, supported: headers };
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 2. Create the resource server and register Stellar testnet
|
|
24
|
+
const resourceServer = new x402ResourceServer(facilitatorClient).register(
|
|
25
|
+
"stellar:testnet",
|
|
26
|
+
new ExactStellarScheme()
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// 3. Create Express app
|
|
30
|
+
const app = express();
|
|
31
|
+
app.use(express.json());
|
|
32
|
+
|
|
33
|
+
// 4. Add payment middleware with route pricing
|
|
34
|
+
app.use(
|
|
35
|
+
paymentMiddleware(
|
|
36
|
+
{
|
|
37
|
+
"POST /api/weather": {
|
|
38
|
+
accepts: [
|
|
39
|
+
{
|
|
40
|
+
scheme: "exact",
|
|
41
|
+
price: "$0.001",
|
|
42
|
+
network: "stellar:testnet",
|
|
43
|
+
payTo: process.env.SERVER_STELLAR_ADDRESS!,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
description: "Get weather data for a city",
|
|
47
|
+
mimeType: "application/json",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
resourceServer
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// 5. Define the actual route handler (runs only after payment verified)
|
|
55
|
+
app.post("/api/weather", (req, res) => {
|
|
56
|
+
const { city } = req.body;
|
|
57
|
+
res.json({
|
|
58
|
+
city,
|
|
59
|
+
temperature: "22°C",
|
|
60
|
+
condition: "Sunny",
|
|
61
|
+
paid: true,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 6. Start server
|
|
66
|
+
const PORT = process.env.PORT || 3000;
|
|
67
|
+
app.listen(PORT, () => {
|
|
68
|
+
console.log(`x402 server running on port ${PORT}`);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Key Server Details
|
|
73
|
+
|
|
74
|
+
- `ExactStellarScheme` on the server takes **no arguments** — it only verifies payments.
|
|
75
|
+
- The `facilitatorClient` must return auth headers for `verify`, `settle`, and `supported`.
|
|
76
|
+
- Route keys in the middleware config must match the HTTP method and path exactly: `"POST /api/weather"`.
|
|
77
|
+
- The `price` field uses dollar-prefixed strings: `"$0.001"`, `"$0.01"`, `"$1.00"`.
|
|
78
|
+
- The `payTo` address must be a valid Stellar public key (G...) with a USDC trustline.
|
|
79
|
+
|
|
80
|
+
## Client Pattern (x402 Payment Client)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { createEd25519Signer } from "@x402/stellar";
|
|
84
|
+
import { ExactStellarScheme } from "@x402/stellar/exact/client";
|
|
85
|
+
import { x402Client, x402HTTPClient } from "@x402/core/client";
|
|
86
|
+
import "dotenv/config";
|
|
87
|
+
|
|
88
|
+
const SERVER_URL = process.env.SERVER_URL || "http://localhost:3000";
|
|
89
|
+
|
|
90
|
+
// 1. Create the signer from the client's secret key
|
|
91
|
+
const signer = createEd25519Signer(process.env.CLIENT_STELLAR_SECRET!);
|
|
92
|
+
|
|
93
|
+
// 2. Create the scheme and register it with the x402 client
|
|
94
|
+
const scheme = new ExactStellarScheme(signer);
|
|
95
|
+
const client = new x402Client().register("stellar:testnet", scheme);
|
|
96
|
+
const httpClient = new x402HTTPClient(client);
|
|
97
|
+
|
|
98
|
+
async function callPaidEndpoint() {
|
|
99
|
+
// 3. Make the initial request (will get 402)
|
|
100
|
+
const initialResponse = await fetch(`${SERVER_URL}/api/weather`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ city: "Buenos Aires" }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (initialResponse.status !== 402) {
|
|
107
|
+
// Either succeeded without payment or got an error
|
|
108
|
+
console.log("Status:", initialResponse.status);
|
|
109
|
+
console.log("Body:", await initialResponse.text());
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 4. Parse the 402 response to get payment requirements
|
|
114
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
115
|
+
(name: string) => initialResponse.headers.get(name)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
console.log("Payment required:", JSON.stringify(paymentRequired, null, 2));
|
|
119
|
+
|
|
120
|
+
// 5. Create the payment payload (signs the transaction)
|
|
121
|
+
const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
|
|
122
|
+
|
|
123
|
+
// 6. Encode the payment into headers
|
|
124
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
125
|
+
|
|
126
|
+
// 7. Resend the request with payment headers
|
|
127
|
+
const paidResponse = await fetch(`${SERVER_URL}/api/weather`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
...paymentHeaders,
|
|
132
|
+
},
|
|
133
|
+
body: JSON.stringify({ city: "Buenos Aires" }),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
console.log("Paid response status:", paidResponse.status);
|
|
137
|
+
console.log("Paid response body:", await paidResponse.json());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
callPaidEndpoint().catch(console.error);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Key Client Details
|
|
144
|
+
|
|
145
|
+
- `ExactStellarScheme` on the client takes the **signer** as an argument.
|
|
146
|
+
- Import from `@x402/stellar/exact/client` — NOT from `@x402/stellar/exact/server`.
|
|
147
|
+
- The `getPaymentRequiredResponse` method takes a header getter function.
|
|
148
|
+
- The `encodePaymentSignatureHeader` returns an object that can be spread into fetch headers.
|
|
149
|
+
- The client account must have USDC balance and a USDC trustline.
|
|
150
|
+
|
|
151
|
+
## Multi-Endpoint Pricing Pattern
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
app.use(
|
|
155
|
+
paymentMiddleware(
|
|
156
|
+
{
|
|
157
|
+
"POST /api/weather": {
|
|
158
|
+
accepts: [
|
|
159
|
+
{
|
|
160
|
+
scheme: "exact",
|
|
161
|
+
price: "$0.001",
|
|
162
|
+
network: "stellar:testnet",
|
|
163
|
+
payTo: process.env.SERVER_STELLAR_ADDRESS!,
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
description: "Get weather data",
|
|
167
|
+
mimeType: "application/json",
|
|
168
|
+
},
|
|
169
|
+
"GET /api/premium-data": {
|
|
170
|
+
accepts: [
|
|
171
|
+
{
|
|
172
|
+
scheme: "exact",
|
|
173
|
+
price: "$0.01",
|
|
174
|
+
network: "stellar:testnet",
|
|
175
|
+
payTo: process.env.SERVER_STELLAR_ADDRESS!,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
description: "Access premium dataset",
|
|
179
|
+
mimeType: "application/json",
|
|
180
|
+
},
|
|
181
|
+
"POST /api/ai-summary": {
|
|
182
|
+
accepts: [
|
|
183
|
+
{
|
|
184
|
+
scheme: "exact",
|
|
185
|
+
price: "$0.05",
|
|
186
|
+
network: "stellar:testnet",
|
|
187
|
+
payTo: process.env.SERVER_STELLAR_ADDRESS!,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
description: "AI-generated summary",
|
|
191
|
+
mimeType: "application/json",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
resourceServer
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Different endpoints can have different prices. Unprotected routes are not listed in the middleware config and work normally without payment.
|
|
200
|
+
|
|
201
|
+
## Error Handling Patterns
|
|
202
|
+
|
|
203
|
+
### Server-Side Error Handling
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Wrap route handlers to catch errors after payment verification
|
|
207
|
+
app.post("/api/weather", async (req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const { city } = req.body;
|
|
210
|
+
if (!city) {
|
|
211
|
+
return res.status(400).json({ error: "City parameter required" });
|
|
212
|
+
}
|
|
213
|
+
// Process and respond
|
|
214
|
+
res.json({ city, temperature: "22°C" });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
console.error("Handler error:", error);
|
|
217
|
+
res.status(500).json({ error: "Internal server error" });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Client-Side Error Handling
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
async function callWithRetry(url: string, options: RequestInit, maxRetries = 2) {
|
|
226
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(url, options);
|
|
229
|
+
|
|
230
|
+
if (response.status === 402) {
|
|
231
|
+
// Parse payment requirements and pay
|
|
232
|
+
const paymentRequired = httpClient.getPaymentRequiredResponse(
|
|
233
|
+
(name: string) => response.headers.get(name)
|
|
234
|
+
);
|
|
235
|
+
const paymentPayload = await httpClient.createPaymentPayload(paymentRequired);
|
|
236
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload);
|
|
237
|
+
|
|
238
|
+
// Resend with payment
|
|
239
|
+
const paidResponse = await fetch(url, {
|
|
240
|
+
...options,
|
|
241
|
+
headers: { ...options.headers, ...paymentHeaders },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!paidResponse.ok) {
|
|
245
|
+
throw new Error(`Payment accepted but request failed: ${paidResponse.status}`);
|
|
246
|
+
}
|
|
247
|
+
return paidResponse;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
throw new Error(`Request failed: ${response.status}`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return response;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (attempt === maxRetries) throw error;
|
|
257
|
+
console.warn(`Attempt ${attempt + 1} failed, retrying...`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Checking Payment Status
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// The payment middleware adds payment info to the request
|
|
267
|
+
// Access it in your route handler if needed
|
|
268
|
+
app.post("/api/weather", (req, res) => {
|
|
269
|
+
// The request only reaches here if payment was verified
|
|
270
|
+
// You can log payment details from the x-payment header
|
|
271
|
+
const paymentHeader = req.headers["x-payment"];
|
|
272
|
+
console.log("Payment received:", paymentHeader ? "yes" : "no");
|
|
273
|
+
|
|
274
|
+
res.json({ city: req.body.city, temperature: "22°C", paid: true });
|
|
275
|
+
});
|
|
276
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# x402-stellar Setup Reference
|
|
2
|
+
|
|
3
|
+
## 1. Create a Stellar Testnet Account
|
|
4
|
+
|
|
5
|
+
Use the Stellar Laboratory to create and fund a testnet account.
|
|
6
|
+
|
|
7
|
+
### Via Stellar Laboratory (recommended)
|
|
8
|
+
|
|
9
|
+
1. Go to https://laboratory.stellar.org/#account-creator?network=test
|
|
10
|
+
2. Click "Generate Keypair" — save both the **Public Key** (G...) and **Secret Key** (S...)
|
|
11
|
+
3. Click "Fund account on testnet" to receive 10,000 test XLM from Friendbot
|
|
12
|
+
|
|
13
|
+
### Via CLI
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
curl -s "https://friendbot.stellar.org?addr=YOUR_PUBLIC_KEY" | jq .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Via Stellar SDK
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Keypair } from "@stellar/stellar-sdk";
|
|
23
|
+
|
|
24
|
+
const keypair = Keypair.random();
|
|
25
|
+
console.log("Public:", keypair.publicKey()); // G...
|
|
26
|
+
console.log("Secret:", keypair.secret()); // S...
|
|
27
|
+
|
|
28
|
+
// Fund on testnet
|
|
29
|
+
await fetch(`https://friendbot.stellar.org?addr=${keypair.publicKey()}`);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You need **two accounts**:
|
|
33
|
+
- **Server account** — receives USDC payments (use the Public Key as `SERVER_STELLAR_ADDRESS`)
|
|
34
|
+
- **Client account** — signs and sends payments (use the Secret Key as `CLIENT_STELLAR_SECRET`)
|
|
35
|
+
|
|
36
|
+
## 2. Get OpenZeppelin Facilitator API Key
|
|
37
|
+
|
|
38
|
+
The facilitator verifies and settles x402 payments on behalf of your server.
|
|
39
|
+
|
|
40
|
+
1. Go to https://channels.openzeppelin.com/testnet/gen
|
|
41
|
+
2. Generate a new API key
|
|
42
|
+
3. Save the key as `FACILITATOR_API_KEY` in your `.env`
|
|
43
|
+
|
|
44
|
+
The facilitator URL for testnet is:
|
|
45
|
+
```
|
|
46
|
+
https://channels.openzeppelin.com/x402/testnet
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For mainnet, use:
|
|
50
|
+
```
|
|
51
|
+
https://channels.openzeppelin.com/x402/mainnet
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 3. Environment Variable Reference
|
|
55
|
+
|
|
56
|
+
| Variable | Format | Description | Where to Get It |
|
|
57
|
+
|----------|--------|-------------|-----------------|
|
|
58
|
+
| `SERVER_STELLAR_ADDRESS` | `G...` (56 chars) | Public key of the account receiving payments | Stellar Laboratory or SDK keypair generation |
|
|
59
|
+
| `FACILITATOR_URL` | URL | OpenZeppelin facilitator endpoint | `https://channels.openzeppelin.com/x402/testnet` |
|
|
60
|
+
| `FACILITATOR_API_KEY` | String | API key for the facilitator service | `channels.openzeppelin.com/testnet/gen` |
|
|
61
|
+
| `CLIENT_STELLAR_SECRET` | `S...` (56 chars) | Secret key of the account making payments | Stellar Laboratory or SDK keypair generation |
|
|
62
|
+
|
|
63
|
+
### Example `.env` file
|
|
64
|
+
|
|
65
|
+
```env
|
|
66
|
+
# Server configuration
|
|
67
|
+
SERVER_STELLAR_ADDRESS=GBZXN7PIRZGNMHGA7MUUUF4GWZPWTQVL6NKRTZSKU74AQKLLP5LCBUS
|
|
68
|
+
FACILITATOR_URL=https://channels.openzeppelin.com/x402/testnet
|
|
69
|
+
FACILITATOR_API_KEY=oz_fac_abc123def456
|
|
70
|
+
|
|
71
|
+
# Client configuration
|
|
72
|
+
CLIENT_STELLAR_SECRET=SCZANGBA5YHTNYVVV3C7CAZMCLP4JMQ3HZRQMIV6ILWLHYZPXTLCWBI
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 4. Fund Testnet Account with USDC
|
|
76
|
+
|
|
77
|
+
Testnet USDC is needed for the client account to make payments.
|
|
78
|
+
|
|
79
|
+
### Step 1: Add USDC Trustline
|
|
80
|
+
|
|
81
|
+
Before an account can hold USDC, it must establish a trustline to the USDC issuer.
|
|
82
|
+
|
|
83
|
+
**Testnet USDC issuer:** `GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5`
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import {
|
|
87
|
+
Keypair,
|
|
88
|
+
Server,
|
|
89
|
+
TransactionBuilder,
|
|
90
|
+
Networks,
|
|
91
|
+
Operation,
|
|
92
|
+
Asset,
|
|
93
|
+
} from "@stellar/stellar-sdk";
|
|
94
|
+
|
|
95
|
+
const server = new Server("https://horizon-testnet.stellar.org");
|
|
96
|
+
const keypair = Keypair.fromSecret(process.env.CLIENT_STELLAR_SECRET!);
|
|
97
|
+
const account = await server.loadAccount(keypair.publicKey());
|
|
98
|
+
|
|
99
|
+
const usdcAsset = new Asset(
|
|
100
|
+
"USDC",
|
|
101
|
+
"GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const tx = new TransactionBuilder(account, {
|
|
105
|
+
fee: "100",
|
|
106
|
+
networkPassphrase: Networks.TESTNET,
|
|
107
|
+
})
|
|
108
|
+
.addOperation(Operation.changeTrust({ asset: usdcAsset }))
|
|
109
|
+
.setTimeout(30)
|
|
110
|
+
.build();
|
|
111
|
+
|
|
112
|
+
tx.sign(keypair);
|
|
113
|
+
await server.submitTransaction(tx);
|
|
114
|
+
console.log("USDC trustline added");
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Step 2: Get Testnet USDC
|
|
118
|
+
|
|
119
|
+
Use the testnet USDC faucet or transfer from a funded testnet account. Check the Stellar Laboratory for testnet USDC distribution tools.
|
|
120
|
+
|
|
121
|
+
You can also use the Stellar Laboratory to manually add a trustline:
|
|
122
|
+
1. Go to https://laboratory.stellar.org/#txbuilder?network=test
|
|
123
|
+
2. Set source account to your client public key
|
|
124
|
+
3. Add a "Change Trust" operation with USDC asset and the testnet issuer
|
|
125
|
+
4. Sign and submit
|
|
126
|
+
|
|
127
|
+
## 5. Verify Setup
|
|
128
|
+
|
|
129
|
+
Run the dependency checker script to validate everything is configured:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node scripts/check-deps.js
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This checks:
|
|
136
|
+
- All @x402 packages are installed
|
|
137
|
+
- All required environment variables are set
|
|
138
|
+
- Variable formats are correct (G... for public keys, S... for secrets)
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* x402-stellar dependency and environment checker
|
|
5
|
+
*
|
|
6
|
+
* Checks that all required @x402 packages are installed and
|
|
7
|
+
* that environment variables are properly configured.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node scripts/check-deps.js
|
|
10
|
+
* Exit codes: 0 = all checks passed, 1 = one or more checks failed
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require("fs");
|
|
14
|
+
const path = require("path");
|
|
15
|
+
|
|
16
|
+
const REQUIRED_PACKAGES = [
|
|
17
|
+
"@x402/express",
|
|
18
|
+
"@x402/stellar",
|
|
19
|
+
"@x402/core",
|
|
20
|
+
"@stellar/stellar-sdk",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const REQUIRED_ENV_VARS = [
|
|
24
|
+
{
|
|
25
|
+
name: "SERVER_STELLAR_ADDRESS",
|
|
26
|
+
description: "Stellar public key for receiving payments",
|
|
27
|
+
validate: (val) => /^G[A-Z0-9]{55}$/.test(val),
|
|
28
|
+
hint: "Must be a Stellar public key starting with G (56 characters)",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "FACILITATOR_URL",
|
|
32
|
+
description: "OpenZeppelin facilitator endpoint URL",
|
|
33
|
+
validate: (val) => val.startsWith("https://"),
|
|
34
|
+
hint: "Use https://channels.openzeppelin.com/x402/testnet for testnet",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "FACILITATOR_API_KEY",
|
|
38
|
+
description: "API key for the facilitator service",
|
|
39
|
+
validate: (val) => val.length > 0,
|
|
40
|
+
hint: "Generate at channels.openzeppelin.com/testnet/gen",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "CLIENT_STELLAR_SECRET",
|
|
44
|
+
description: "Stellar secret key for signing payments",
|
|
45
|
+
validate: (val) => /^S[A-Z0-9]{55}$/.test(val),
|
|
46
|
+
hint: "Must be a Stellar secret key starting with S (56 characters)",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function checkPackages() {
|
|
51
|
+
const checks = [];
|
|
52
|
+
let packageJson;
|
|
53
|
+
|
|
54
|
+
// Find package.json by walking up from cwd
|
|
55
|
+
let dir = process.cwd();
|
|
56
|
+
let packageJsonPath = null;
|
|
57
|
+
|
|
58
|
+
while (dir !== path.dirname(dir)) {
|
|
59
|
+
const candidate = path.join(dir, "package.json");
|
|
60
|
+
if (fs.existsSync(candidate)) {
|
|
61
|
+
packageJsonPath = candidate;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
dir = path.dirname(dir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!packageJsonPath) {
|
|
68
|
+
checks.push({
|
|
69
|
+
type: "package",
|
|
70
|
+
name: "package.json",
|
|
71
|
+
status: "error",
|
|
72
|
+
message: "No package.json found in current directory or parent directories",
|
|
73
|
+
});
|
|
74
|
+
return checks;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
checks.push({
|
|
81
|
+
type: "package",
|
|
82
|
+
name: "package.json",
|
|
83
|
+
status: "error",
|
|
84
|
+
message: `Failed to parse package.json: ${err.message}`,
|
|
85
|
+
});
|
|
86
|
+
return checks;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const allDeps = {
|
|
90
|
+
...packageJson.dependencies,
|
|
91
|
+
...packageJson.devDependencies,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const pkg of REQUIRED_PACKAGES) {
|
|
95
|
+
if (allDeps && allDeps[pkg]) {
|
|
96
|
+
checks.push({
|
|
97
|
+
type: "package",
|
|
98
|
+
name: pkg,
|
|
99
|
+
status: "ok",
|
|
100
|
+
message: `Installed (${allDeps[pkg]})`,
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
checks.push({
|
|
104
|
+
type: "package",
|
|
105
|
+
name: pkg,
|
|
106
|
+
status: "error",
|
|
107
|
+
message: `Not found in package.json. Run: npm install ${pkg}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return checks;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkEnvVars() {
|
|
116
|
+
const checks = [];
|
|
117
|
+
|
|
118
|
+
// Try to load .env file
|
|
119
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
120
|
+
const envVars = { ...process.env };
|
|
121
|
+
|
|
122
|
+
if (fs.existsSync(envPath)) {
|
|
123
|
+
try {
|
|
124
|
+
const envContent = fs.readFileSync(envPath, "utf8");
|
|
125
|
+
const lines = envContent.split("\n");
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const trimmed = line.trim();
|
|
128
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
129
|
+
const eqIndex = trimmed.indexOf("=");
|
|
130
|
+
if (eqIndex === -1) continue;
|
|
131
|
+
const key = trimmed.substring(0, eqIndex).trim();
|
|
132
|
+
let value = trimmed.substring(eqIndex + 1).trim();
|
|
133
|
+
// Remove surrounding quotes
|
|
134
|
+
if (
|
|
135
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
136
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
137
|
+
) {
|
|
138
|
+
value = value.slice(1, -1);
|
|
139
|
+
}
|
|
140
|
+
envVars[key] = value;
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
checks.push({
|
|
144
|
+
type: "env",
|
|
145
|
+
name: ".env",
|
|
146
|
+
status: "error",
|
|
147
|
+
message: `Failed to read .env file: ${err.message}`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
checks.push({
|
|
152
|
+
type: "env",
|
|
153
|
+
name: ".env",
|
|
154
|
+
status: "warning",
|
|
155
|
+
message: "No .env file found. Environment variables must be set in the shell.",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const envVar of REQUIRED_ENV_VARS) {
|
|
160
|
+
const value = envVars[envVar.name];
|
|
161
|
+
if (!value) {
|
|
162
|
+
checks.push({
|
|
163
|
+
type: "env",
|
|
164
|
+
name: envVar.name,
|
|
165
|
+
status: "error",
|
|
166
|
+
message: `Not set. ${envVar.description}. ${envVar.hint}`,
|
|
167
|
+
});
|
|
168
|
+
} else if (!envVar.validate(value)) {
|
|
169
|
+
checks.push({
|
|
170
|
+
type: "env",
|
|
171
|
+
name: envVar.name,
|
|
172
|
+
status: "error",
|
|
173
|
+
message: `Invalid format. ${envVar.hint}`,
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
const masked = value.substring(0, 4) + "..." + value.substring(value.length - 4);
|
|
177
|
+
checks.push({
|
|
178
|
+
type: "env",
|
|
179
|
+
name: envVar.name,
|
|
180
|
+
status: "ok",
|
|
181
|
+
message: `Set (${masked})`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return checks;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function main() {
|
|
190
|
+
const packageChecks = checkPackages();
|
|
191
|
+
const envChecks = checkEnvVars();
|
|
192
|
+
const allChecks = [...packageChecks, ...envChecks];
|
|
193
|
+
|
|
194
|
+
const hasErrors = allChecks.some((c) => c.status === "error");
|
|
195
|
+
const result = {
|
|
196
|
+
status: hasErrors ? "error" : "ok",
|
|
197
|
+
checks: allChecks,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Pretty print to stderr for humans
|
|
201
|
+
console.error("\nx402-stellar Dependency Check");
|
|
202
|
+
console.error("=".repeat(40));
|
|
203
|
+
|
|
204
|
+
for (const check of allChecks) {
|
|
205
|
+
const icon = check.status === "ok" ? "[OK]" : check.status === "warning" ? "[WARN]" : "[ERR]";
|
|
206
|
+
console.error(`${icon} ${check.name}: ${check.message}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.error("=".repeat(40));
|
|
210
|
+
console.error(hasErrors ? "Some checks failed.\n" : "All checks passed.\n");
|
|
211
|
+
|
|
212
|
+
// JSON to stdout for programmatic use
|
|
213
|
+
console.log(JSON.stringify(result, null, 2));
|
|
214
|
+
|
|
215
|
+
process.exit(hasErrors ? 1 : 0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
main();
|