powertools-x402 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/LICENSE +21 -0
- package/README.md +185 -0
- package/dist/index.cjs +233 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +210 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Allen Helton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# powertools-x402
|
|
2
|
+
|
|
3
|
+
Paid API routes for [AWS Lambda Powertools Event Handler](https://docs.powertools.aws.dev/lambda/typescript/latest/features/event-handler/api-gateway/) using the [x402 payment protocol](https://x402.org).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install powertools-x402
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node 18+. `@aws-lambda-powertools/event-handler` is a peer dependency, so npm will grab it automatically if your app doesn't already have it.
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
`x402.paid()` is route middleware that turns any route into a paid endpoint. The route owns the price. Your handler stays pure business logic.
|
|
16
|
+
|
|
17
|
+
- No payment attached? The caller gets a 402 with signed payment requirements
|
|
18
|
+
- Payment attached? It gets verified with a facilitator before your handler runs
|
|
19
|
+
- Settlement happens after your handler succeeds. If it throws or returns an error status, the caller is never charged
|
|
20
|
+
- Verified payment details (payer, amount, network) are available in the request store
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
### Charge for a route
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Router } from '@aws-lambda-powertools/event-handler/http';
|
|
28
|
+
import type { Context } from 'aws-lambda';
|
|
29
|
+
import { createX402, type X402Environment } from 'powertools-x402';
|
|
30
|
+
|
|
31
|
+
const app = new Router<X402Environment>();
|
|
32
|
+
|
|
33
|
+
const x402 = createX402({
|
|
34
|
+
facilitator: 'https://x402.org/facilitator',
|
|
35
|
+
network: 'eip155:84532', // Base Sepolia
|
|
36
|
+
payTo: process.env.PAY_TO!,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
app.get('/health', async () => ({ status: 'ok' }));
|
|
40
|
+
|
|
41
|
+
app.post(
|
|
42
|
+
'/summarize',
|
|
43
|
+
[x402.paid({ price: '$0.01', description: 'Summarize some text' })],
|
|
44
|
+
async (reqCtx) => {
|
|
45
|
+
const { text } = (await reqCtx.req.json()) as { text: string };
|
|
46
|
+
return { summary: text.slice(0, 80) };
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const handler = (event: unknown, context: Context) => app.resolve(event, context);
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Read payment details in the handler
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
app.post('/generate', [x402.paid({ price: '$0.05' })], async (reqCtx) => {
|
|
57
|
+
const payment = reqCtx.get('payment');
|
|
58
|
+
// { network: 'eip155:84532', scheme: 'exact', asset: '0x...', amount: '50000', payer: '0x...' }
|
|
59
|
+
return { result: 'expensive thing', paidBy: payment?.payer };
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Emit CloudWatch metrics
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
const x402 = createX402({
|
|
67
|
+
facilitator: 'https://x402.org/facilitator',
|
|
68
|
+
network: 'eip155:84532',
|
|
69
|
+
payTo: process.env.PAY_TO!,
|
|
70
|
+
enableMetrics: true, // defaults to false
|
|
71
|
+
logger: new Logger(), // optional, any Powertools Logger works
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This emits `PaymentRequired`, `PaymentRejected`, `PaymentVerified`, `PaymentSettled`, `PaymentCancelled`, and `SettlementFailed` counts under the `x402` namespace. Metrics publish immediately, so there's no `publishStoredMetrics()` call to remember. Pass your own `metrics` instance if you want a different namespace.
|
|
76
|
+
|
|
77
|
+
### Use an authenticated facilitator
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const x402 = createX402({
|
|
81
|
+
facilitator: {
|
|
82
|
+
url: 'https://facilitator.example.com',
|
|
83
|
+
createAuthHeaders: async () => {
|
|
84
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
85
|
+
return { verify: headers, settle: headers, supported: headers };
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
network: 'eip155:8453', // Base mainnet
|
|
89
|
+
payTo: process.env.PAY_TO!,
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Accept multiple payment options on one route
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
x402.paid({
|
|
97
|
+
accepts: [
|
|
98
|
+
{ scheme: 'exact', network: 'eip155:8453', payTo: evmAddress, price: '$0.05' },
|
|
99
|
+
{ scheme: 'exact', network: 'eip155:84532', payTo: evmAddress, price: '$0.01' },
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Want to take payments on non-EVM networks? Register additional schemes with the `schemes` option on `createX402`.
|
|
105
|
+
|
|
106
|
+
### Let some callers through free
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
x402.paid({
|
|
110
|
+
price: '$0.01',
|
|
111
|
+
onProtectedRequest: async (ctx) => {
|
|
112
|
+
if (ctx.adapter.getHeader('x-api-key') === trustedKey) return { grantAccess: true };
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Customize the 402 response
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
x402.paid({
|
|
121
|
+
price: '$0.01',
|
|
122
|
+
resource: 'https://api.example.com/summarize', // canonical URL behind custom domains
|
|
123
|
+
unpaidResponseBody: async () => ({
|
|
124
|
+
contentType: 'application/json',
|
|
125
|
+
body: { preview: 'The first 10 words are free...' },
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Test your routes without a network
|
|
131
|
+
|
|
132
|
+
The `facilitator` option takes any object with `verify`, `settle`, and `getSupported`, so your tests never touch the network:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const facilitator = {
|
|
136
|
+
getSupported: async () => ({
|
|
137
|
+
kinds: [{ x402Version: 2, scheme: 'exact', network: 'eip155:84532' }],
|
|
138
|
+
extensions: [],
|
|
139
|
+
signers: {},
|
|
140
|
+
}),
|
|
141
|
+
verify: async () => ({ isValid: true, payer: '0xPayer' }),
|
|
142
|
+
settle: async () => ({ success: true, transaction: '0xTx', network: 'eip155:84532' }),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const x402 = createX402({ facilitator, network: 'eip155:84532', payTo: '0xYou' });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Drive your router with API Gateway events through `app.resolve(event, context)`. Check out [test/middleware.test.ts](test/middleware.test.ts) for full round trips, including client-side payment signing.
|
|
149
|
+
|
|
150
|
+
### Pay for a request (client side)
|
|
151
|
+
|
|
152
|
+
In the paying app, install `@x402/core`, `@x402/evm`, and `viem`:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { x402Client, x402HTTPClient } from '@x402/core/client';
|
|
156
|
+
import { registerExactEvmScheme } from '@x402/evm/exact/client';
|
|
157
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
158
|
+
|
|
159
|
+
const client = new x402HTTPClient(
|
|
160
|
+
registerExactEvmScheme(new x402Client(), { signer: privateKeyToAccount(privateKey) })
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// 1. Call without payment -> 402 with a PAYMENT-REQUIRED header
|
|
164
|
+
const challenge = await fetch(url, { method: 'POST', body });
|
|
165
|
+
|
|
166
|
+
// 2. Sign a payment for one of the offered options
|
|
167
|
+
const paymentRequired = client.getPaymentRequiredResponse((name) => challenge.headers.get(name));
|
|
168
|
+
const payment = await client.createPaymentPayload(paymentRequired);
|
|
169
|
+
|
|
170
|
+
// 3. Retry with the PAYMENT-SIGNATURE header -> 200 + settlement receipt
|
|
171
|
+
const paid = await fetch(url, { method: 'POST', headers: client.encodePaymentSignatureHeader(payment), body });
|
|
172
|
+
const receipt = client.getPaymentSettleResponse((name) => paid.headers.get(name));
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Need testnet USDC? Grab some from the [Circle faucet](https://faucet.circle.com/).
|
|
176
|
+
|
|
177
|
+
## Examples
|
|
178
|
+
|
|
179
|
+
- [example/handler.ts](example/handler.ts) - lambdalith with free and paid routes
|
|
180
|
+
- [example/client.ts](example/client.ts) - paying client, step by step
|
|
181
|
+
- [example/template.yaml](example/template.yaml) - SAM deploy (esbuild, ESM, HTTP API)
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CachingFacilitatorClient: () => CachingFacilitatorClient,
|
|
24
|
+
PowertoolsAdapter: () => PowertoolsAdapter,
|
|
25
|
+
createX402: () => createX402
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_metrics = require("@aws-lambda-powertools/metrics");
|
|
29
|
+
var import_server = require("@x402/core/server");
|
|
30
|
+
var import_server2 = require("@x402/evm/exact/server");
|
|
31
|
+
|
|
32
|
+
// src/adapter.ts
|
|
33
|
+
var PowertoolsAdapter = class {
|
|
34
|
+
constructor(request) {
|
|
35
|
+
this.request = request;
|
|
36
|
+
}
|
|
37
|
+
request;
|
|
38
|
+
getHeader(name) {
|
|
39
|
+
return this.request.headers.get(name) ?? void 0;
|
|
40
|
+
}
|
|
41
|
+
getMethod() {
|
|
42
|
+
return this.request.method;
|
|
43
|
+
}
|
|
44
|
+
getPath() {
|
|
45
|
+
return new URL(this.request.url).pathname;
|
|
46
|
+
}
|
|
47
|
+
getUrl() {
|
|
48
|
+
return this.request.url;
|
|
49
|
+
}
|
|
50
|
+
getAcceptHeader() {
|
|
51
|
+
return this.request.headers.get("accept") ?? "";
|
|
52
|
+
}
|
|
53
|
+
getUserAgent() {
|
|
54
|
+
return this.request.headers.get("user-agent") ?? "";
|
|
55
|
+
}
|
|
56
|
+
getQueryParams() {
|
|
57
|
+
const params = new URL(this.request.url).searchParams;
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const key of new Set(params.keys())) {
|
|
60
|
+
const values = params.getAll(key);
|
|
61
|
+
result[key] = values.length === 1 ? values[0] : values;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
getQueryParam(name) {
|
|
66
|
+
const values = new URL(this.request.url).searchParams.getAll(name);
|
|
67
|
+
if (values.length === 0) return void 0;
|
|
68
|
+
return values.length === 1 ? values[0] : values;
|
|
69
|
+
}
|
|
70
|
+
async getBody() {
|
|
71
|
+
try {
|
|
72
|
+
return await this.request.clone().json();
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/facilitator.ts
|
|
80
|
+
var CachingFacilitatorClient = class {
|
|
81
|
+
constructor(client) {
|
|
82
|
+
this.client = client;
|
|
83
|
+
}
|
|
84
|
+
client;
|
|
85
|
+
#supported;
|
|
86
|
+
verify(payload, requirements) {
|
|
87
|
+
return this.client.verify(payload, requirements);
|
|
88
|
+
}
|
|
89
|
+
settle(payload, requirements) {
|
|
90
|
+
return this.client.settle(payload, requirements);
|
|
91
|
+
}
|
|
92
|
+
getSupported() {
|
|
93
|
+
this.#supported ??= this.client.getSupported().catch((error) => {
|
|
94
|
+
this.#supported = void 0;
|
|
95
|
+
throw error;
|
|
96
|
+
});
|
|
97
|
+
return this.#supported;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// src/index.ts
|
|
102
|
+
var toFacilitatorClient = (facilitator) => {
|
|
103
|
+
if (typeof facilitator === "string") return new import_server.HTTPFacilitatorClient({ url: facilitator });
|
|
104
|
+
if ("getSupported" in facilitator) return facilitator;
|
|
105
|
+
return new import_server.HTTPFacilitatorClient(facilitator);
|
|
106
|
+
};
|
|
107
|
+
var toResponse = ({ status, headers, body, isHtml }) => new Response(isHtml ? String(body ?? "") : JSON.stringify(body ?? {}), { status, headers });
|
|
108
|
+
function createX402(options) {
|
|
109
|
+
const facilitator = new CachingFacilitatorClient(toFacilitatorClient(options.facilitator));
|
|
110
|
+
const resourceServer = new import_server.x402ResourceServer(facilitator);
|
|
111
|
+
for (const register of options.schemes ?? [import_server2.registerExactEvmScheme]) {
|
|
112
|
+
register(resourceServer);
|
|
113
|
+
}
|
|
114
|
+
const { logger } = options;
|
|
115
|
+
const metrics = options.enableMetrics ? options.metrics ?? new import_metrics.Metrics({ namespace: "x402" }) : void 0;
|
|
116
|
+
const count = (name) => metrics && (metrics.singleMetric?.() ?? metrics).addMetric(name, "Count", 1);
|
|
117
|
+
const payers = /* @__PURE__ */ new WeakMap();
|
|
118
|
+
resourceServer.onAfterVerify(async ({ paymentPayload, result }) => {
|
|
119
|
+
if (result.payer) payers.set(paymentPayload, result.payer);
|
|
120
|
+
});
|
|
121
|
+
function paid(route) {
|
|
122
|
+
const { price, accepts, onProtectedRequest, ...routeConfig } = route;
|
|
123
|
+
if (accepts === void 0 && price === void 0) {
|
|
124
|
+
throw new Error("paid() requires a price or an accepts configuration");
|
|
125
|
+
}
|
|
126
|
+
const httpServer = new import_server.x402HTTPResourceServer(resourceServer, {
|
|
127
|
+
...routeConfig,
|
|
128
|
+
mimeType: routeConfig.mimeType ?? "application/json",
|
|
129
|
+
accepts: accepts ?? {
|
|
130
|
+
scheme: "exact",
|
|
131
|
+
network: options.network,
|
|
132
|
+
payTo: options.payTo,
|
|
133
|
+
price
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
if (onProtectedRequest) httpServer.onProtectedRequest(onProtectedRequest);
|
|
137
|
+
let ready;
|
|
138
|
+
const initialize = () => ready ??= httpServer.initialize().catch((error) => {
|
|
139
|
+
ready = void 0;
|
|
140
|
+
throw error;
|
|
141
|
+
});
|
|
142
|
+
return async ({ reqCtx, next }) => {
|
|
143
|
+
await initialize();
|
|
144
|
+
const adapter = new PowertoolsAdapter(reqCtx.req);
|
|
145
|
+
const context = {
|
|
146
|
+
adapter,
|
|
147
|
+
path: adapter.getPath(),
|
|
148
|
+
method: adapter.getMethod()
|
|
149
|
+
};
|
|
150
|
+
const result = await httpServer.processHTTPRequest(context, options.paywall);
|
|
151
|
+
if (result.type === "no-payment-required") {
|
|
152
|
+
await next();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (result.type === "payment-error") {
|
|
156
|
+
count(adapter.getHeader("payment-signature") ? "PaymentRejected" : "PaymentRequired");
|
|
157
|
+
logger?.debug("x402 payment not accepted", { status: result.response.status });
|
|
158
|
+
reqCtx.res = toResponse(result.response);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const {
|
|
162
|
+
cancellationDispatcher,
|
|
163
|
+
beforeHandlerSettlement,
|
|
164
|
+
paymentPayload,
|
|
165
|
+
paymentRequirements,
|
|
166
|
+
declaredExtensions
|
|
167
|
+
} = result;
|
|
168
|
+
reqCtx.set("payment", {
|
|
169
|
+
network: paymentRequirements.network,
|
|
170
|
+
scheme: paymentRequirements.scheme,
|
|
171
|
+
asset: paymentRequirements.asset,
|
|
172
|
+
amount: paymentRequirements.amount,
|
|
173
|
+
payer: payers.get(paymentPayload)
|
|
174
|
+
});
|
|
175
|
+
count("PaymentVerified");
|
|
176
|
+
try {
|
|
177
|
+
await next();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
await cancellationDispatcher.cancel({ reason: "handler_threw", error });
|
|
180
|
+
count("PaymentCancelled");
|
|
181
|
+
logger?.warn("x402 payment cancelled: handler threw", { path: context.path });
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
if (reqCtx.res.status >= 400) {
|
|
185
|
+
await cancellationDispatcher.cancel({
|
|
186
|
+
reason: "handler_failed",
|
|
187
|
+
responseStatus: reqCtx.res.status
|
|
188
|
+
});
|
|
189
|
+
count("PaymentCancelled");
|
|
190
|
+
logger?.warn("x402 payment cancelled: handler failed", {
|
|
191
|
+
path: context.path,
|
|
192
|
+
status: reqCtx.res.status
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const settlement = await httpServer.processSettlement(
|
|
197
|
+
paymentPayload,
|
|
198
|
+
paymentRequirements,
|
|
199
|
+
declaredExtensions,
|
|
200
|
+
{
|
|
201
|
+
request: context,
|
|
202
|
+
responseBody: Buffer.from(await reqCtx.res.clone().arrayBuffer()),
|
|
203
|
+
responseHeaders: Object.fromEntries(reqCtx.res.headers.entries())
|
|
204
|
+
},
|
|
205
|
+
void 0,
|
|
206
|
+
beforeHandlerSettlement
|
|
207
|
+
);
|
|
208
|
+
if (!settlement.success) {
|
|
209
|
+
count("SettlementFailed");
|
|
210
|
+
logger?.error("x402 settlement failed", {
|
|
211
|
+
path: context.path,
|
|
212
|
+
errorReason: settlement.errorReason
|
|
213
|
+
});
|
|
214
|
+
reqCtx.res = toResponse(settlement.response);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
count("PaymentSettled");
|
|
218
|
+
logger?.debug("x402 payment settled", { path: context.path, transaction: settlement.transaction });
|
|
219
|
+
for (const [name, value] of Object.entries(settlement.headers)) {
|
|
220
|
+
reqCtx.res.headers.set(name, value);
|
|
221
|
+
}
|
|
222
|
+
const cacheControl = reqCtx.res.headers.get("cache-control");
|
|
223
|
+
reqCtx.res.headers.set("cache-control", cacheControl ? `${cacheControl}, private` : "private");
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { paid, resourceServer };
|
|
227
|
+
}
|
|
228
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
229
|
+
0 && (module.exports = {
|
|
230
|
+
CachingFacilitatorClient,
|
|
231
|
+
PowertoolsAdapter,
|
|
232
|
+
createX402
|
|
233
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Middleware } from '@aws-lambda-powertools/event-handler/types';
|
|
2
|
+
import { HTTPAdapter, FacilitatorClient, FacilitatorConfig, x402ResourceServer, PaywallConfig, RouteConfig, ProtectedRequestHook } from '@x402/core/server';
|
|
3
|
+
import { PaymentOption } from '@x402/core/http';
|
|
4
|
+
import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, SupportedResponse, Network, Price } from '@x402/core/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bridges the web-standard Request exposed by Powertools Event Handler
|
|
8
|
+
* to x402's framework-agnostic HTTPAdapter.
|
|
9
|
+
*/
|
|
10
|
+
declare class PowertoolsAdapter implements HTTPAdapter {
|
|
11
|
+
private readonly request;
|
|
12
|
+
constructor(request: Request);
|
|
13
|
+
getHeader(name: string): string | undefined;
|
|
14
|
+
getMethod(): string;
|
|
15
|
+
getPath(): string;
|
|
16
|
+
getUrl(): string;
|
|
17
|
+
getAcceptHeader(): string;
|
|
18
|
+
getUserAgent(): string;
|
|
19
|
+
getQueryParams(): Record<string, string | string[]>;
|
|
20
|
+
getQueryParam(name: string): string | string[] | undefined;
|
|
21
|
+
getBody(): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Memoizes getSupported() so every paid route on a cold start shares one
|
|
26
|
+
* facilitator round trip instead of re-fetching per route.
|
|
27
|
+
*/
|
|
28
|
+
declare class CachingFacilitatorClient implements FacilitatorClient {
|
|
29
|
+
#private;
|
|
30
|
+
private readonly client;
|
|
31
|
+
constructor(client: FacilitatorClient);
|
|
32
|
+
verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise<VerifyResponse>;
|
|
33
|
+
settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise<SettleResponse>;
|
|
34
|
+
getSupported(): Promise<SupportedResponse>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface X402Logger {
|
|
38
|
+
debug(message: string, extra: Record<string, unknown>): void;
|
|
39
|
+
warn(message: string, extra: Record<string, unknown>): void;
|
|
40
|
+
error(message: string, extra: Record<string, unknown>): void;
|
|
41
|
+
}
|
|
42
|
+
interface X402Metrics {
|
|
43
|
+
addMetric(name: string, unit: 'Count', value: number): void;
|
|
44
|
+
singleMetric?(): Pick<X402Metrics, 'addMetric'>;
|
|
45
|
+
}
|
|
46
|
+
type SchemeRegistrar = (server: x402ResourceServer) => unknown;
|
|
47
|
+
interface CreateX402Options {
|
|
48
|
+
facilitator: string | FacilitatorConfig | FacilitatorClient;
|
|
49
|
+
network: Network;
|
|
50
|
+
payTo: string;
|
|
51
|
+
schemes?: SchemeRegistrar[];
|
|
52
|
+
paywall?: PaywallConfig;
|
|
53
|
+
logger?: X402Logger;
|
|
54
|
+
enableMetrics?: boolean;
|
|
55
|
+
metrics?: X402Metrics;
|
|
56
|
+
}
|
|
57
|
+
interface PaidRouteOptions extends Omit<RouteConfig, 'accepts'> {
|
|
58
|
+
price?: Price;
|
|
59
|
+
accepts?: PaymentOption | PaymentOption[];
|
|
60
|
+
onProtectedRequest?: ProtectedRequestHook;
|
|
61
|
+
}
|
|
62
|
+
type PaymentInfo = {
|
|
63
|
+
network: Network;
|
|
64
|
+
scheme: string;
|
|
65
|
+
asset: string;
|
|
66
|
+
amount: string;
|
|
67
|
+
payer?: string;
|
|
68
|
+
};
|
|
69
|
+
type X402Environment = {
|
|
70
|
+
store: {
|
|
71
|
+
request: {
|
|
72
|
+
payment?: PaymentInfo;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
declare function createX402(options: CreateX402Options): {
|
|
77
|
+
paid: (route: PaidRouteOptions) => Middleware<X402Environment>;
|
|
78
|
+
resourceServer: x402ResourceServer;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Middleware } from '@aws-lambda-powertools/event-handler/types';
|
|
2
|
+
import { HTTPAdapter, FacilitatorClient, FacilitatorConfig, x402ResourceServer, PaywallConfig, RouteConfig, ProtectedRequestHook } from '@x402/core/server';
|
|
3
|
+
import { PaymentOption } from '@x402/core/http';
|
|
4
|
+
import { PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse, SupportedResponse, Network, Price } from '@x402/core/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Bridges the web-standard Request exposed by Powertools Event Handler
|
|
8
|
+
* to x402's framework-agnostic HTTPAdapter.
|
|
9
|
+
*/
|
|
10
|
+
declare class PowertoolsAdapter implements HTTPAdapter {
|
|
11
|
+
private readonly request;
|
|
12
|
+
constructor(request: Request);
|
|
13
|
+
getHeader(name: string): string | undefined;
|
|
14
|
+
getMethod(): string;
|
|
15
|
+
getPath(): string;
|
|
16
|
+
getUrl(): string;
|
|
17
|
+
getAcceptHeader(): string;
|
|
18
|
+
getUserAgent(): string;
|
|
19
|
+
getQueryParams(): Record<string, string | string[]>;
|
|
20
|
+
getQueryParam(name: string): string | string[] | undefined;
|
|
21
|
+
getBody(): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Memoizes getSupported() so every paid route on a cold start shares one
|
|
26
|
+
* facilitator round trip instead of re-fetching per route.
|
|
27
|
+
*/
|
|
28
|
+
declare class CachingFacilitatorClient implements FacilitatorClient {
|
|
29
|
+
#private;
|
|
30
|
+
private readonly client;
|
|
31
|
+
constructor(client: FacilitatorClient);
|
|
32
|
+
verify(payload: PaymentPayload, requirements: PaymentRequirements): Promise<VerifyResponse>;
|
|
33
|
+
settle(payload: PaymentPayload, requirements: PaymentRequirements): Promise<SettleResponse>;
|
|
34
|
+
getSupported(): Promise<SupportedResponse>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface X402Logger {
|
|
38
|
+
debug(message: string, extra: Record<string, unknown>): void;
|
|
39
|
+
warn(message: string, extra: Record<string, unknown>): void;
|
|
40
|
+
error(message: string, extra: Record<string, unknown>): void;
|
|
41
|
+
}
|
|
42
|
+
interface X402Metrics {
|
|
43
|
+
addMetric(name: string, unit: 'Count', value: number): void;
|
|
44
|
+
singleMetric?(): Pick<X402Metrics, 'addMetric'>;
|
|
45
|
+
}
|
|
46
|
+
type SchemeRegistrar = (server: x402ResourceServer) => unknown;
|
|
47
|
+
interface CreateX402Options {
|
|
48
|
+
facilitator: string | FacilitatorConfig | FacilitatorClient;
|
|
49
|
+
network: Network;
|
|
50
|
+
payTo: string;
|
|
51
|
+
schemes?: SchemeRegistrar[];
|
|
52
|
+
paywall?: PaywallConfig;
|
|
53
|
+
logger?: X402Logger;
|
|
54
|
+
enableMetrics?: boolean;
|
|
55
|
+
metrics?: X402Metrics;
|
|
56
|
+
}
|
|
57
|
+
interface PaidRouteOptions extends Omit<RouteConfig, 'accepts'> {
|
|
58
|
+
price?: Price;
|
|
59
|
+
accepts?: PaymentOption | PaymentOption[];
|
|
60
|
+
onProtectedRequest?: ProtectedRequestHook;
|
|
61
|
+
}
|
|
62
|
+
type PaymentInfo = {
|
|
63
|
+
network: Network;
|
|
64
|
+
scheme: string;
|
|
65
|
+
asset: string;
|
|
66
|
+
amount: string;
|
|
67
|
+
payer?: string;
|
|
68
|
+
};
|
|
69
|
+
type X402Environment = {
|
|
70
|
+
store: {
|
|
71
|
+
request: {
|
|
72
|
+
payment?: PaymentInfo;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
declare function createX402(options: CreateX402Options): {
|
|
77
|
+
paid: (route: PaidRouteOptions) => Middleware<X402Environment>;
|
|
78
|
+
resourceServer: x402ResourceServer;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export { CachingFacilitatorClient, type CreateX402Options, type PaidRouteOptions, type PaymentInfo, PowertoolsAdapter, type SchemeRegistrar, type X402Environment, type X402Logger, type X402Metrics, createX402 };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Metrics } from "@aws-lambda-powertools/metrics";
|
|
3
|
+
import {
|
|
4
|
+
HTTPFacilitatorClient,
|
|
5
|
+
x402HTTPResourceServer,
|
|
6
|
+
x402ResourceServer
|
|
7
|
+
} from "@x402/core/server";
|
|
8
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/server";
|
|
9
|
+
|
|
10
|
+
// src/adapter.ts
|
|
11
|
+
var PowertoolsAdapter = class {
|
|
12
|
+
constructor(request) {
|
|
13
|
+
this.request = request;
|
|
14
|
+
}
|
|
15
|
+
request;
|
|
16
|
+
getHeader(name) {
|
|
17
|
+
return this.request.headers.get(name) ?? void 0;
|
|
18
|
+
}
|
|
19
|
+
getMethod() {
|
|
20
|
+
return this.request.method;
|
|
21
|
+
}
|
|
22
|
+
getPath() {
|
|
23
|
+
return new URL(this.request.url).pathname;
|
|
24
|
+
}
|
|
25
|
+
getUrl() {
|
|
26
|
+
return this.request.url;
|
|
27
|
+
}
|
|
28
|
+
getAcceptHeader() {
|
|
29
|
+
return this.request.headers.get("accept") ?? "";
|
|
30
|
+
}
|
|
31
|
+
getUserAgent() {
|
|
32
|
+
return this.request.headers.get("user-agent") ?? "";
|
|
33
|
+
}
|
|
34
|
+
getQueryParams() {
|
|
35
|
+
const params = new URL(this.request.url).searchParams;
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const key of new Set(params.keys())) {
|
|
38
|
+
const values = params.getAll(key);
|
|
39
|
+
result[key] = values.length === 1 ? values[0] : values;
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
getQueryParam(name) {
|
|
44
|
+
const values = new URL(this.request.url).searchParams.getAll(name);
|
|
45
|
+
if (values.length === 0) return void 0;
|
|
46
|
+
return values.length === 1 ? values[0] : values;
|
|
47
|
+
}
|
|
48
|
+
async getBody() {
|
|
49
|
+
try {
|
|
50
|
+
return await this.request.clone().json();
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/facilitator.ts
|
|
58
|
+
var CachingFacilitatorClient = class {
|
|
59
|
+
constructor(client) {
|
|
60
|
+
this.client = client;
|
|
61
|
+
}
|
|
62
|
+
client;
|
|
63
|
+
#supported;
|
|
64
|
+
verify(payload, requirements) {
|
|
65
|
+
return this.client.verify(payload, requirements);
|
|
66
|
+
}
|
|
67
|
+
settle(payload, requirements) {
|
|
68
|
+
return this.client.settle(payload, requirements);
|
|
69
|
+
}
|
|
70
|
+
getSupported() {
|
|
71
|
+
this.#supported ??= this.client.getSupported().catch((error) => {
|
|
72
|
+
this.#supported = void 0;
|
|
73
|
+
throw error;
|
|
74
|
+
});
|
|
75
|
+
return this.#supported;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/index.ts
|
|
80
|
+
var toFacilitatorClient = (facilitator) => {
|
|
81
|
+
if (typeof facilitator === "string") return new HTTPFacilitatorClient({ url: facilitator });
|
|
82
|
+
if ("getSupported" in facilitator) return facilitator;
|
|
83
|
+
return new HTTPFacilitatorClient(facilitator);
|
|
84
|
+
};
|
|
85
|
+
var toResponse = ({ status, headers, body, isHtml }) => new Response(isHtml ? String(body ?? "") : JSON.stringify(body ?? {}), { status, headers });
|
|
86
|
+
function createX402(options) {
|
|
87
|
+
const facilitator = new CachingFacilitatorClient(toFacilitatorClient(options.facilitator));
|
|
88
|
+
const resourceServer = new x402ResourceServer(facilitator);
|
|
89
|
+
for (const register of options.schemes ?? [registerExactEvmScheme]) {
|
|
90
|
+
register(resourceServer);
|
|
91
|
+
}
|
|
92
|
+
const { logger } = options;
|
|
93
|
+
const metrics = options.enableMetrics ? options.metrics ?? new Metrics({ namespace: "x402" }) : void 0;
|
|
94
|
+
const count = (name) => metrics && (metrics.singleMetric?.() ?? metrics).addMetric(name, "Count", 1);
|
|
95
|
+
const payers = /* @__PURE__ */ new WeakMap();
|
|
96
|
+
resourceServer.onAfterVerify(async ({ paymentPayload, result }) => {
|
|
97
|
+
if (result.payer) payers.set(paymentPayload, result.payer);
|
|
98
|
+
});
|
|
99
|
+
function paid(route) {
|
|
100
|
+
const { price, accepts, onProtectedRequest, ...routeConfig } = route;
|
|
101
|
+
if (accepts === void 0 && price === void 0) {
|
|
102
|
+
throw new Error("paid() requires a price or an accepts configuration");
|
|
103
|
+
}
|
|
104
|
+
const httpServer = new x402HTTPResourceServer(resourceServer, {
|
|
105
|
+
...routeConfig,
|
|
106
|
+
mimeType: routeConfig.mimeType ?? "application/json",
|
|
107
|
+
accepts: accepts ?? {
|
|
108
|
+
scheme: "exact",
|
|
109
|
+
network: options.network,
|
|
110
|
+
payTo: options.payTo,
|
|
111
|
+
price
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
if (onProtectedRequest) httpServer.onProtectedRequest(onProtectedRequest);
|
|
115
|
+
let ready;
|
|
116
|
+
const initialize = () => ready ??= httpServer.initialize().catch((error) => {
|
|
117
|
+
ready = void 0;
|
|
118
|
+
throw error;
|
|
119
|
+
});
|
|
120
|
+
return async ({ reqCtx, next }) => {
|
|
121
|
+
await initialize();
|
|
122
|
+
const adapter = new PowertoolsAdapter(reqCtx.req);
|
|
123
|
+
const context = {
|
|
124
|
+
adapter,
|
|
125
|
+
path: adapter.getPath(),
|
|
126
|
+
method: adapter.getMethod()
|
|
127
|
+
};
|
|
128
|
+
const result = await httpServer.processHTTPRequest(context, options.paywall);
|
|
129
|
+
if (result.type === "no-payment-required") {
|
|
130
|
+
await next();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (result.type === "payment-error") {
|
|
134
|
+
count(adapter.getHeader("payment-signature") ? "PaymentRejected" : "PaymentRequired");
|
|
135
|
+
logger?.debug("x402 payment not accepted", { status: result.response.status });
|
|
136
|
+
reqCtx.res = toResponse(result.response);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const {
|
|
140
|
+
cancellationDispatcher,
|
|
141
|
+
beforeHandlerSettlement,
|
|
142
|
+
paymentPayload,
|
|
143
|
+
paymentRequirements,
|
|
144
|
+
declaredExtensions
|
|
145
|
+
} = result;
|
|
146
|
+
reqCtx.set("payment", {
|
|
147
|
+
network: paymentRequirements.network,
|
|
148
|
+
scheme: paymentRequirements.scheme,
|
|
149
|
+
asset: paymentRequirements.asset,
|
|
150
|
+
amount: paymentRequirements.amount,
|
|
151
|
+
payer: payers.get(paymentPayload)
|
|
152
|
+
});
|
|
153
|
+
count("PaymentVerified");
|
|
154
|
+
try {
|
|
155
|
+
await next();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await cancellationDispatcher.cancel({ reason: "handler_threw", error });
|
|
158
|
+
count("PaymentCancelled");
|
|
159
|
+
logger?.warn("x402 payment cancelled: handler threw", { path: context.path });
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
if (reqCtx.res.status >= 400) {
|
|
163
|
+
await cancellationDispatcher.cancel({
|
|
164
|
+
reason: "handler_failed",
|
|
165
|
+
responseStatus: reqCtx.res.status
|
|
166
|
+
});
|
|
167
|
+
count("PaymentCancelled");
|
|
168
|
+
logger?.warn("x402 payment cancelled: handler failed", {
|
|
169
|
+
path: context.path,
|
|
170
|
+
status: reqCtx.res.status
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const settlement = await httpServer.processSettlement(
|
|
175
|
+
paymentPayload,
|
|
176
|
+
paymentRequirements,
|
|
177
|
+
declaredExtensions,
|
|
178
|
+
{
|
|
179
|
+
request: context,
|
|
180
|
+
responseBody: Buffer.from(await reqCtx.res.clone().arrayBuffer()),
|
|
181
|
+
responseHeaders: Object.fromEntries(reqCtx.res.headers.entries())
|
|
182
|
+
},
|
|
183
|
+
void 0,
|
|
184
|
+
beforeHandlerSettlement
|
|
185
|
+
);
|
|
186
|
+
if (!settlement.success) {
|
|
187
|
+
count("SettlementFailed");
|
|
188
|
+
logger?.error("x402 settlement failed", {
|
|
189
|
+
path: context.path,
|
|
190
|
+
errorReason: settlement.errorReason
|
|
191
|
+
});
|
|
192
|
+
reqCtx.res = toResponse(settlement.response);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
count("PaymentSettled");
|
|
196
|
+
logger?.debug("x402 payment settled", { path: context.path, transaction: settlement.transaction });
|
|
197
|
+
for (const [name, value] of Object.entries(settlement.headers)) {
|
|
198
|
+
reqCtx.res.headers.set(name, value);
|
|
199
|
+
}
|
|
200
|
+
const cacheControl = reqCtx.res.headers.get("cache-control");
|
|
201
|
+
reqCtx.res.headers.set("cache-control", cacheControl ? `${cacheControl}, private` : "private");
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return { paid, resourceServer };
|
|
205
|
+
}
|
|
206
|
+
export {
|
|
207
|
+
CachingFacilitatorClient,
|
|
208
|
+
PowertoolsAdapter,
|
|
209
|
+
createX402
|
|
210
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "powertools-x402",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "x402 payment middleware for AWS Lambda Powertools Event Handler",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Allen Helton <allenheltondev@gmail.com>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/allenheltondev/powertools-x402.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"x402",
|
|
13
|
+
"payments",
|
|
14
|
+
"aws-lambda",
|
|
15
|
+
"powertools",
|
|
16
|
+
"middleware",
|
|
17
|
+
"serverless"
|
|
18
|
+
],
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"import": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"require": {
|
|
30
|
+
"types": "./dist/index.d.cts",
|
|
31
|
+
"default": "./dist/index.cjs"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsup",
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:coverage": "vitest run --coverage --coverage.include=src/**",
|
|
47
|
+
"prepublishOnly": "npm run typecheck && npm run test && npm run build"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@aws-lambda-powertools/metrics": "^2.35.0",
|
|
51
|
+
"@x402/core": "^2.23.0",
|
|
52
|
+
"@x402/evm": "^2.23.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@aws-lambda-powertools/event-handler": "^2.35.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@aws-lambda-powertools/event-handler": "^2.35.0",
|
|
59
|
+
"@aws-lambda-powertools/logger": "^2.35.0",
|
|
60
|
+
"@types/aws-lambda": "^8.10.152",
|
|
61
|
+
"@types/node": "^24.0.0",
|
|
62
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
63
|
+
"tsup": "^8.4.0",
|
|
64
|
+
"typescript": "^5.7.0",
|
|
65
|
+
"viem": "^2.48.0",
|
|
66
|
+
"vitest": "^3.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|