l402-server 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 +133 -0
- package/dist/index.cjs +267 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +300 -0
- package/dist/index.d.ts +300 -0
- package/dist/index.js +235 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 refined-element
|
|
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,133 @@
|
|
|
1
|
+
# l402-server
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/l402-server)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**L402 server SDK for Node.** Mint Lightning invoices and macaroons. Verify L402 tokens. Wrap any HTTP API with pay-per-request Lightning payments — one `npm install`, two methods.
|
|
7
|
+
|
|
8
|
+
This is the **producer-side** companion to [`l402-requests`](https://www.npmjs.com/package/l402-requests) (the consumer-side auto-paying HTTP client). Use `l402-requests` to *call* paid APIs from agents. Use `l402-server` to *build* paid APIs that those agents pay for.
|
|
9
|
+
|
|
10
|
+
## What you're paying for
|
|
11
|
+
|
|
12
|
+
`l402-server` is a thin TypeScript wrapper around [Lightning Enable](https://lightningenable.com)'s hosted producer API. The protocol-heavy work — invoice minting, macaroon signing, preimage verification, replay protection, wallet integration (Strike / OpenNode / LND / NWC) — all runs on Lightning Enable's side. The SDK is ~200 lines of HTTP-client glue.
|
|
13
|
+
|
|
14
|
+
**Requires a Lightning Enable merchant API key** and an Agentic Commerce subscription ($99/mo Individual or $299/mo Business). Get both at [lightningenable.com/dashboard](https://api.lightningenable.com/dashboard).
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install l402-server
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Node 18+. ESM + CJS dual exports. TypeScript types included.
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { L402Server } from "l402-server";
|
|
28
|
+
|
|
29
|
+
const l402 = new L402Server({
|
|
30
|
+
apiKey: process.env.LIGHTNING_ENABLE_API_KEY!,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 1. On an unauthenticated incoming request, mint a challenge:
|
|
34
|
+
const challenge = await l402.createChallenge({
|
|
35
|
+
resource: "/api/premium/weather",
|
|
36
|
+
priceSats: 100,
|
|
37
|
+
description: "Premium weather forecast",
|
|
38
|
+
});
|
|
39
|
+
// → { invoice: "lnbc100n1...", macaroon: "AgEL...", paymentHash: "...", expiresAt, ... }
|
|
40
|
+
|
|
41
|
+
// Send back as 402 Payment Required with the macaroon + invoice in headers.
|
|
42
|
+
// (Use a framework middleware on top of this SDK to automate that step.)
|
|
43
|
+
|
|
44
|
+
// 2. When the client retries with Authorization: L402 <macaroon>:<preimage>,
|
|
45
|
+
// parse the header and verify:
|
|
46
|
+
const result = await l402.verifyToken({
|
|
47
|
+
macaroon: parsedMacaroon,
|
|
48
|
+
preimage: parsedPreimage,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (result.valid) {
|
|
52
|
+
// result.resource → which path the token is bound to
|
|
53
|
+
// result.amountSats → how much was paid
|
|
54
|
+
// Serve your real response.
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Surface
|
|
59
|
+
|
|
60
|
+
### `new L402Server(options)`
|
|
61
|
+
|
|
62
|
+
| Option | Type | Default | Notes |
|
|
63
|
+
|---|---|---|---|
|
|
64
|
+
| `apiKey` | `string` | **required** | Your Lightning Enable merchant API key |
|
|
65
|
+
| `baseUrl` | `string` | `https://api.lightningenable.com` | Override for testing |
|
|
66
|
+
| `timeoutMs` | `number` | `10000` | Per-request timeout |
|
|
67
|
+
| `fetch` | `typeof fetch` | global `fetch` | Inject for tests / retries |
|
|
68
|
+
|
|
69
|
+
### `createChallenge(args)` → `Promise<Challenge>`
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
{
|
|
73
|
+
resource: string; // bound as a macaroon caveat
|
|
74
|
+
priceSats: number; // ≥ 1
|
|
75
|
+
description?: string; // shown in payer's wallet
|
|
76
|
+
idempotencyKey?: string; // optional; falls back to client IP
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Returns: `{ invoice, macaroon, paymentHash, expiresAt, resource, priceSats, mppChallenge? }`.
|
|
81
|
+
|
|
82
|
+
### `verifyToken(args)` → `Promise<VerificationResult>`
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
{
|
|
86
|
+
macaroon?: string; // required for L402; omit only for MPP
|
|
87
|
+
preimage: string;
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Returns: `{ valid, error?, resource?, merchantId?, amountSats?, paymentHash? }`. Inspect `result.valid` — the producer API returns 200 OK for both valid and invalid tokens.
|
|
92
|
+
|
|
93
|
+
### Errors
|
|
94
|
+
|
|
95
|
+
All SDK errors extend `L402ServerError`:
|
|
96
|
+
|
|
97
|
+
| Class | When |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `L402AuthError` | 401 — API key missing / invalid / revoked |
|
|
100
|
+
| `L402PlanError` | 403 — L402 not enabled on merchant's plan (surfaces `currentPlan`) |
|
|
101
|
+
| `L402ApiError` | Other non-2xx (400 / 429 / 5xx); surfaces `statusCode` + `responseBody` |
|
|
102
|
+
| `L402NetworkError` | Timeout, DNS, TLS, transport failure (surfaces `cause`) |
|
|
103
|
+
|
|
104
|
+
## Two integration modes
|
|
105
|
+
|
|
106
|
+
Lightning Enable supports two integration shapes:
|
|
107
|
+
|
|
108
|
+
- **Proxy mode** — point Lightning Enable at your API URL; we forward authenticated requests on your behalf. Best for public APIs or quick experiments. [Setup walkthrough](https://docs.lightningenable.com/products/l402-microtransactions/proxy-setup-walkthrough).
|
|
109
|
+
- **Native mode** — install this SDK in your existing API. Lightning Enable handles payment; your API handles everything else. Best for commercial APIs with their own auth, observability, or sensitive infrastructure. **This SDK is the Native mode building block.**
|
|
110
|
+
|
|
111
|
+
Framework-specific middleware that wraps this SDK is in development:
|
|
112
|
+
|
|
113
|
+
- `l402-express` — Express middleware
|
|
114
|
+
- ASP.NET Core middleware (separate package)
|
|
115
|
+
- FastAPI dependency (separate package)
|
|
116
|
+
- `l402-server-go` — Go middleware (Phase 2 of the roadmap)
|
|
117
|
+
|
|
118
|
+
## Architectural notes
|
|
119
|
+
|
|
120
|
+
The architectural decisions baked into this SDK are deliberate:
|
|
121
|
+
|
|
122
|
+
- **No protocol code in the SDK.** Macaroon signing, preimage hashing, payment-hash linking — all server-side. The SDK is HTTP-client glue.
|
|
123
|
+
- **Verification via the hosted endpoint, not local key material.** We don't distribute the L402 root key to merchants. Every `verifyToken` call goes to `/api/l402/challenges/verify`. One round-trip per paid request (~50ms regional). The trade-off is acceptable until a high-volume partner complains; we'll revisit then.
|
|
124
|
+
- **Replay prevention centralized.** Lightning Enable tracks consumed preimages. Merchants don't maintain a local cache. Consistent across the merchant's whole API surface.
|
|
125
|
+
- **No credentials stored anywhere.** Lightning Enable never asks for your upstream API credentials. The SDK just calls our two endpoints; your traffic stays on your server.
|
|
126
|
+
|
|
127
|
+
## Contributing
|
|
128
|
+
|
|
129
|
+
Open source under MIT. Issues and pull requests welcome. For protocol-level discussion, see the [L402 spec at lightninglabs/L402](https://github.com/lightninglabs/L402).
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT © Refined Element, LLC
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
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
|
+
L402ApiError: () => L402ApiError,
|
|
24
|
+
L402AuthError: () => L402AuthError,
|
|
25
|
+
L402NetworkError: () => L402NetworkError,
|
|
26
|
+
L402PlanError: () => L402PlanError,
|
|
27
|
+
L402Server: () => L402Server,
|
|
28
|
+
L402ServerError: () => L402ServerError
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/errors.ts
|
|
33
|
+
var L402ServerError = class extends Error {
|
|
34
|
+
constructor(message, options) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
this.name = "L402ServerError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var L402AuthError = class extends L402ServerError {
|
|
40
|
+
constructor(message = "Merchant API key is missing or invalid.") {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "L402AuthError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var L402PlanError = class extends L402ServerError {
|
|
46
|
+
/**
|
|
47
|
+
* The plan tier currently on the merchant (e.g., "starter").
|
|
48
|
+
* Populated when the server includes it in the error payload.
|
|
49
|
+
*/
|
|
50
|
+
currentPlan;
|
|
51
|
+
constructor(message = "L402 is not enabled on this merchant's plan.", currentPlan) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "L402PlanError";
|
|
54
|
+
this.currentPlan = currentPlan;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var L402NetworkError = class extends L402ServerError {
|
|
58
|
+
constructor(message, cause) {
|
|
59
|
+
super(message, { cause });
|
|
60
|
+
this.name = "L402NetworkError";
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
var L402ApiError = class extends L402ServerError {
|
|
64
|
+
/**
|
|
65
|
+
* HTTP status code returned by the producer API.
|
|
66
|
+
*/
|
|
67
|
+
statusCode;
|
|
68
|
+
/**
|
|
69
|
+
* Raw response body, useful for debugging. May be a parsed object or a
|
|
70
|
+
* string if parsing failed.
|
|
71
|
+
*/
|
|
72
|
+
responseBody;
|
|
73
|
+
constructor(statusCode, message, responseBody) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "L402ApiError";
|
|
76
|
+
this.statusCode = statusCode;
|
|
77
|
+
this.responseBody = responseBody;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/L402Server.ts
|
|
82
|
+
var DEFAULT_BASE_URL = "https://api.lightningenable.com";
|
|
83
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
84
|
+
var L402Server = class {
|
|
85
|
+
apiKey;
|
|
86
|
+
baseUrl;
|
|
87
|
+
timeoutMs;
|
|
88
|
+
fetchImpl;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
if (!options.apiKey || options.apiKey.trim().length === 0) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
"L402Server: `apiKey` is required. Get one from your Lightning Enable dashboard."
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
this.apiKey = options.apiKey;
|
|
96
|
+
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
97
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
98
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mint an L402 challenge — a Lightning invoice plus a macaroon scoped to
|
|
102
|
+
* the given resource. Present this to the requesting client/agent in a
|
|
103
|
+
* `402 Payment Required` response. Once they pay the invoice and obtain
|
|
104
|
+
* the preimage, they will retry the request with
|
|
105
|
+
* `Authorization: L402 <macaroon>:<preimage>`.
|
|
106
|
+
*
|
|
107
|
+
* @param args - resource path, price in sats, optional description and idempotency key.
|
|
108
|
+
* @returns The {@link Challenge} containing the invoice, macaroon, and metadata.
|
|
109
|
+
* @throws {@link L402AuthError} on 401 (invalid API key).
|
|
110
|
+
* @throws {@link L402PlanError} on 403 (L402 not enabled on merchant's plan).
|
|
111
|
+
* @throws {@link L402ApiError} on other non-2xx responses.
|
|
112
|
+
* @throws {@link L402NetworkError} on timeout or transport failure.
|
|
113
|
+
*/
|
|
114
|
+
async createChallenge(args) {
|
|
115
|
+
if (!args.resource || args.resource.trim().length === 0) {
|
|
116
|
+
throw new Error("createChallenge: `resource` is required.");
|
|
117
|
+
}
|
|
118
|
+
if (!Number.isFinite(args.priceSats) || args.priceSats < 1) {
|
|
119
|
+
throw new Error("createChallenge: `priceSats` must be an integer \u2265 1.");
|
|
120
|
+
}
|
|
121
|
+
const headers = {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"X-API-Key": this.apiKey,
|
|
124
|
+
Accept: "application/json"
|
|
125
|
+
};
|
|
126
|
+
if (args.idempotencyKey) {
|
|
127
|
+
headers["X-Idempotency-Key"] = args.idempotencyKey;
|
|
128
|
+
}
|
|
129
|
+
const body = JSON.stringify({
|
|
130
|
+
resource: args.resource,
|
|
131
|
+
priceSats: args.priceSats,
|
|
132
|
+
description: args.description
|
|
133
|
+
});
|
|
134
|
+
const response = await this.request(
|
|
135
|
+
"/api/l402/challenges",
|
|
136
|
+
"POST",
|
|
137
|
+
headers,
|
|
138
|
+
body
|
|
139
|
+
);
|
|
140
|
+
if (response.status === 200) {
|
|
141
|
+
const data = await response.json();
|
|
142
|
+
return {
|
|
143
|
+
invoice: data.invoice,
|
|
144
|
+
macaroon: data.macaroon,
|
|
145
|
+
paymentHash: data.paymentHash,
|
|
146
|
+
expiresAt: data.expiresAt,
|
|
147
|
+
resource: data.resource,
|
|
148
|
+
priceSats: data.priceSats,
|
|
149
|
+
mppChallenge: data.mppChallenge ?? void 0
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
await this.throwForStatus(response);
|
|
153
|
+
throw new L402ApiError(
|
|
154
|
+
response.status,
|
|
155
|
+
"Unexpected response from L402 producer API."
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Verify an L402 token. Returns a {@link VerificationResult} indicating
|
|
160
|
+
* whether the token is valid plus metadata extracted from the macaroon
|
|
161
|
+
* (resource, merchant ID, amount).
|
|
162
|
+
*
|
|
163
|
+
* The producer API returns `200 OK` for both valid and invalid tokens;
|
|
164
|
+
* inspect `result.valid` rather than relying on HTTP status. Non-200
|
|
165
|
+
* responses indicate a higher-level problem (auth, plan, transport).
|
|
166
|
+
*
|
|
167
|
+
* @param args - macaroon (required for L402; omit only for MPP) + preimage.
|
|
168
|
+
* @returns The {@link VerificationResult}.
|
|
169
|
+
* @throws {@link L402AuthError} on 401 (invalid API key).
|
|
170
|
+
* @throws {@link L402PlanError} on 403 (L402 not enabled on merchant's plan).
|
|
171
|
+
* @throws {@link L402ApiError} on other non-2xx responses.
|
|
172
|
+
* @throws {@link L402NetworkError} on timeout or transport failure.
|
|
173
|
+
*/
|
|
174
|
+
async verifyToken(args) {
|
|
175
|
+
if (!args.preimage || args.preimage.trim().length === 0) {
|
|
176
|
+
throw new Error("verifyToken: `preimage` is required.");
|
|
177
|
+
}
|
|
178
|
+
const headers = {
|
|
179
|
+
"Content-Type": "application/json",
|
|
180
|
+
"X-API-Key": this.apiKey,
|
|
181
|
+
Accept: "application/json"
|
|
182
|
+
};
|
|
183
|
+
const body = JSON.stringify({
|
|
184
|
+
macaroon: args.macaroon,
|
|
185
|
+
preimage: args.preimage
|
|
186
|
+
});
|
|
187
|
+
const response = await this.request(
|
|
188
|
+
"/api/l402/challenges/verify",
|
|
189
|
+
"POST",
|
|
190
|
+
headers,
|
|
191
|
+
body
|
|
192
|
+
);
|
|
193
|
+
if (response.status === 200) {
|
|
194
|
+
const data = await response.json();
|
|
195
|
+
return {
|
|
196
|
+
valid: data.valid,
|
|
197
|
+
error: data.error ?? void 0,
|
|
198
|
+
resource: data.resource ?? void 0,
|
|
199
|
+
merchantId: data.merchantId ?? void 0,
|
|
200
|
+
amountSats: data.amountSats ?? void 0,
|
|
201
|
+
paymentHash: data.paymentHash ?? void 0
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
await this.throwForStatus(response);
|
|
205
|
+
throw new L402ApiError(
|
|
206
|
+
response.status,
|
|
207
|
+
"Unexpected response from L402 producer API."
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
async request(path, method, headers, body) {
|
|
211
|
+
const url = `${this.baseUrl}${path}`;
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
214
|
+
try {
|
|
215
|
+
return await this.fetchImpl(url, {
|
|
216
|
+
method,
|
|
217
|
+
headers,
|
|
218
|
+
body,
|
|
219
|
+
signal: controller.signal
|
|
220
|
+
});
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err?.name === "AbortError") {
|
|
223
|
+
throw new L402NetworkError(
|
|
224
|
+
`Request to ${url} timed out after ${this.timeoutMs}ms`,
|
|
225
|
+
err
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
throw new L402NetworkError(
|
|
229
|
+
`Network error talking to ${url}: ${err.message}`,
|
|
230
|
+
err
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
clearTimeout(timer);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async throwForStatus(response) {
|
|
237
|
+
let body;
|
|
238
|
+
try {
|
|
239
|
+
body = await response.json();
|
|
240
|
+
} catch {
|
|
241
|
+
try {
|
|
242
|
+
body = await response.text();
|
|
243
|
+
} catch {
|
|
244
|
+
body = void 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const errorMessage = body?.error ?? body?.message ?? `HTTP ${response.status} from ${response.url}`;
|
|
248
|
+
if (response.status === 401) {
|
|
249
|
+
throw new L402AuthError(errorMessage);
|
|
250
|
+
}
|
|
251
|
+
if (response.status === 403) {
|
|
252
|
+
const currentPlan = body?.current_plan;
|
|
253
|
+
throw new L402PlanError(errorMessage, currentPlan);
|
|
254
|
+
}
|
|
255
|
+
throw new L402ApiError(response.status, errorMessage, body);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
259
|
+
0 && (module.exports = {
|
|
260
|
+
L402ApiError,
|
|
261
|
+
L402AuthError,
|
|
262
|
+
L402NetworkError,
|
|
263
|
+
L402PlanError,
|
|
264
|
+
L402Server,
|
|
265
|
+
L402ServerError
|
|
266
|
+
});
|
|
267
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/L402Server.ts"],"sourcesContent":["export { L402Server } from \"./L402Server.js\";\nexport {\n L402ApiError,\n L402AuthError,\n L402NetworkError,\n L402PlanError,\n L402ServerError,\n} from \"./errors.js\";\nexport type {\n Challenge,\n CreateChallengeArgs,\n L402ServerOptions,\n VerificationResult,\n VerifyTokenArgs,\n} from \"./types.js\";\n","/**\n * Base error class for all SDK-thrown errors. Distinguishable from arbitrary\n * `Error` instances via `instanceof L402ServerError`.\n */\nexport class L402ServerError extends Error {\n constructor(message: string, options?: ErrorOptions) {\n super(message, options);\n this.name = \"L402ServerError\";\n }\n}\n\n/**\n * Thrown on `401 Unauthorized` from the producer API. Almost always means\n * the merchant API key is missing, malformed, expired, or revoked.\n */\nexport class L402AuthError extends L402ServerError {\n constructor(message: string = \"Merchant API key is missing or invalid.\") {\n super(message);\n this.name = \"L402AuthError\";\n }\n}\n\n/**\n * Thrown on `403 Forbidden` from the producer API. Means the merchant\n * exists and the key is valid, but L402 is not enabled on their plan.\n * The merchant needs to upgrade to an Agentic Commerce plan.\n */\nexport class L402PlanError extends L402ServerError {\n /**\n * The plan tier currently on the merchant (e.g., \"starter\").\n * Populated when the server includes it in the error payload.\n */\n readonly currentPlan?: string;\n\n constructor(\n message: string = \"L402 is not enabled on this merchant's plan.\",\n currentPlan?: string,\n ) {\n super(message);\n this.name = \"L402PlanError\";\n this.currentPlan = currentPlan;\n }\n}\n\n/**\n * Thrown for transport-level failures: timeout, DNS error, TLS error,\n * unreachable host. The `cause` carries the original error.\n */\nexport class L402NetworkError extends L402ServerError {\n constructor(message: string, cause?: unknown) {\n super(message, { cause });\n this.name = \"L402NetworkError\";\n }\n}\n\n/**\n * Thrown when the server returns a non-success status that doesn't map to\n * a more specific error class above (e.g., 400 with a request-validation\n * problem, 500 from upstream wallet failure, 429 from rate limiting).\n */\nexport class L402ApiError extends L402ServerError {\n /**\n * HTTP status code returned by the producer API.\n */\n readonly statusCode: number;\n\n /**\n * Raw response body, useful for debugging. May be a parsed object or a\n * string if parsing failed.\n */\n readonly responseBody?: unknown;\n\n constructor(\n statusCode: number,\n message: string,\n responseBody?: unknown,\n ) {\n super(message);\n this.name = \"L402ApiError\";\n this.statusCode = statusCode;\n this.responseBody = responseBody;\n }\n}\n","import {\n L402ApiError,\n L402AuthError,\n L402NetworkError,\n L402PlanError,\n} from \"./errors.js\";\nimport type {\n Challenge,\n CreateChallengeArgs,\n L402ServerOptions,\n VerificationResult,\n VerifyTokenArgs,\n} from \"./types.js\";\n\nconst DEFAULT_BASE_URL = \"https://api.lightningenable.com\";\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n/**\n * Server-side client for Lightning Enable's L402 producer API. Wraps two\n * endpoints:\n *\n * - {@link createChallenge} → `POST /api/l402/challenges` — mint a\n * Lightning invoice + macaroon for a given resource and price.\n * - {@link verifyToken} → `POST /api/l402/challenges/verify` — validate\n * an incoming L402 token (macaroon + preimage).\n *\n * **No protocol logic lives in this SDK.** The Lightning Enable backend\n * signs macaroons, mints invoices, verifies preimages, and tracks consumed\n * tokens for replay protection. The SDK is purely an HTTP client with\n * typed inputs/outputs.\n *\n * @example\n * ```ts\n * import { L402Server } from \"l402-server\";\n *\n * const l402 = new L402Server({\n * apiKey: process.env.LIGHTNING_ENABLE_API_KEY!,\n * });\n *\n * // On an unauthenticated incoming request:\n * const challenge = await l402.createChallenge({\n * resource: \"/api/premium/weather\",\n * priceSats: 100,\n * description: \"Premium weather forecast\",\n * });\n *\n * // Send back as 402 Payment Required with the challenge headers.\n *\n * // When client comes back with Authorization: L402 mac:preimage,\n * // parse and verify:\n * const verification = await l402.verifyToken({\n * macaroon: parsedMacaroon,\n * preimage: parsedPreimage,\n * });\n * if (verification.valid) {\n * // Serve the response.\n * }\n * ```\n */\nexport class L402Server {\n private readonly apiKey: string;\n private readonly baseUrl: string;\n private readonly timeoutMs: number;\n private readonly fetchImpl: typeof fetch;\n\n constructor(options: L402ServerOptions) {\n if (!options.apiKey || options.apiKey.trim().length === 0) {\n throw new Error(\n \"L402Server: `apiKey` is required. Get one from your Lightning Enable dashboard.\",\n );\n }\n\n this.apiKey = options.apiKey;\n this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\\/+$/, \"\");\n this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n this.fetchImpl = options.fetch ?? fetch;\n }\n\n /**\n * Mint an L402 challenge — a Lightning invoice plus a macaroon scoped to\n * the given resource. Present this to the requesting client/agent in a\n * `402 Payment Required` response. Once they pay the invoice and obtain\n * the preimage, they will retry the request with\n * `Authorization: L402 <macaroon>:<preimage>`.\n *\n * @param args - resource path, price in sats, optional description and idempotency key.\n * @returns The {@link Challenge} containing the invoice, macaroon, and metadata.\n * @throws {@link L402AuthError} on 401 (invalid API key).\n * @throws {@link L402PlanError} on 403 (L402 not enabled on merchant's plan).\n * @throws {@link L402ApiError} on other non-2xx responses.\n * @throws {@link L402NetworkError} on timeout or transport failure.\n */\n async createChallenge(args: CreateChallengeArgs): Promise<Challenge> {\n if (!args.resource || args.resource.trim().length === 0) {\n throw new Error(\"createChallenge: `resource` is required.\");\n }\n if (!Number.isFinite(args.priceSats) || args.priceSats < 1) {\n throw new Error(\"createChallenge: `priceSats` must be an integer ≥ 1.\");\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.apiKey,\n Accept: \"application/json\",\n };\n if (args.idempotencyKey) {\n headers[\"X-Idempotency-Key\"] = args.idempotencyKey;\n }\n\n const body = JSON.stringify({\n resource: args.resource,\n priceSats: args.priceSats,\n description: args.description,\n });\n\n const response = await this.request(\n \"/api/l402/challenges\",\n \"POST\",\n headers,\n body,\n );\n\n if (response.status === 200) {\n const data = (await response.json()) as {\n invoice: string;\n macaroon: string;\n paymentHash: string;\n expiresAt: string;\n resource: string;\n priceSats: number;\n mppChallenge?: string | null;\n };\n return {\n invoice: data.invoice,\n macaroon: data.macaroon,\n paymentHash: data.paymentHash,\n expiresAt: data.expiresAt,\n resource: data.resource,\n priceSats: data.priceSats,\n mppChallenge: data.mppChallenge ?? undefined,\n };\n }\n\n await this.throwForStatus(response);\n // Unreachable — throwForStatus always throws on non-2xx.\n throw new L402ApiError(\n response.status,\n \"Unexpected response from L402 producer API.\",\n );\n }\n\n /**\n * Verify an L402 token. Returns a {@link VerificationResult} indicating\n * whether the token is valid plus metadata extracted from the macaroon\n * (resource, merchant ID, amount).\n *\n * The producer API returns `200 OK` for both valid and invalid tokens;\n * inspect `result.valid` rather than relying on HTTP status. Non-200\n * responses indicate a higher-level problem (auth, plan, transport).\n *\n * @param args - macaroon (required for L402; omit only for MPP) + preimage.\n * @returns The {@link VerificationResult}.\n * @throws {@link L402AuthError} on 401 (invalid API key).\n * @throws {@link L402PlanError} on 403 (L402 not enabled on merchant's plan).\n * @throws {@link L402ApiError} on other non-2xx responses.\n * @throws {@link L402NetworkError} on timeout or transport failure.\n */\n async verifyToken(args: VerifyTokenArgs): Promise<VerificationResult> {\n if (!args.preimage || args.preimage.trim().length === 0) {\n throw new Error(\"verifyToken: `preimage` is required.\");\n }\n\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n \"X-API-Key\": this.apiKey,\n Accept: \"application/json\",\n };\n\n const body = JSON.stringify({\n macaroon: args.macaroon,\n preimage: args.preimage,\n });\n\n const response = await this.request(\n \"/api/l402/challenges/verify\",\n \"POST\",\n headers,\n body,\n );\n\n if (response.status === 200) {\n const data = (await response.json()) as {\n valid: boolean;\n resource?: string | null;\n merchantId?: number | null;\n amountSats?: number | null;\n paymentHash?: string | null;\n error?: string | null;\n };\n return {\n valid: data.valid,\n error: data.error ?? undefined,\n resource: data.resource ?? undefined,\n merchantId: data.merchantId ?? undefined,\n amountSats: data.amountSats ?? undefined,\n paymentHash: data.paymentHash ?? undefined,\n };\n }\n\n await this.throwForStatus(response);\n throw new L402ApiError(\n response.status,\n \"Unexpected response from L402 producer API.\",\n );\n }\n\n private async request(\n path: string,\n method: string,\n headers: Record<string, string>,\n body: string,\n ): Promise<Response> {\n const url = `${this.baseUrl}${path}`;\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), this.timeoutMs);\n\n try {\n return await this.fetchImpl(url, {\n method,\n headers,\n body,\n signal: controller.signal,\n });\n } catch (err) {\n if ((err as { name?: string })?.name === \"AbortError\") {\n throw new L402NetworkError(\n `Request to ${url} timed out after ${this.timeoutMs}ms`,\n err,\n );\n }\n throw new L402NetworkError(\n `Network error talking to ${url}: ${(err as Error).message}`,\n err,\n );\n } finally {\n clearTimeout(timer);\n }\n }\n\n private async throwForStatus(response: Response): Promise<never> {\n let body: unknown;\n try {\n body = await response.json();\n } catch {\n try {\n body = await response.text();\n } catch {\n body = undefined;\n }\n }\n\n const errorMessage =\n (body as { error?: string; message?: string })?.error ??\n (body as { error?: string; message?: string })?.message ??\n `HTTP ${response.status} from ${response.url}`;\n\n if (response.status === 401) {\n throw new L402AuthError(errorMessage);\n }\n if (response.status === 403) {\n const currentPlan = (body as { current_plan?: string })?.current_plan;\n throw new L402PlanError(errorMessage, currentPlan);\n }\n throw new L402ApiError(response.status, errorMessage, body);\n }\n}\n"],"mappings":"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","names":[]}
|