aipp-node 1.0.2 → 1.2.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/dist/index.d.mts CHANGED
@@ -6,16 +6,22 @@ interface ChargeParams {
6
6
  amountSats?: number;
7
7
  amountUsd?: number;
8
8
  memo?: string;
9
+ protocol?: 'L402' | 'x402';
9
10
  }
10
11
  interface ChargeResponse {
11
- payment_request: string;
12
12
  payment_hash: string;
13
- amount_sats: number;
13
+ protocol: 'L402' | 'x402';
14
+ amount_usd?: number;
15
+ pay_to?: string;
16
+ network?: string;
17
+ token?: string;
18
+ payment_request?: string;
19
+ amount_sats?: number;
14
20
  }
15
21
  interface ChargeStatus {
22
+ paid: boolean;
16
23
  status: 'pending' | 'settled';
17
- payment_hash: string;
18
- amount_sats: number;
24
+ preimage: string | null;
19
25
  }
20
26
  interface AippErrorResponse {
21
27
  error: string;
@@ -24,6 +30,7 @@ interface AippErrorResponse {
24
30
  interface PayoutResponse {
25
31
  message: string;
26
32
  amount_sats?: number;
33
+ amount_usd?: number;
27
34
  }
28
35
 
29
36
  declare class Aipp {
@@ -32,17 +39,33 @@ declare class Aipp {
32
39
  constructor(config: AippConfig);
33
40
  private request;
34
41
  /**
35
- * Creates a new Lightning Invoice
42
+ * Creates a new Invoice (either L402 or x402)
36
43
  */
37
44
  createCharge(params: ChargeParams): Promise<ChargeResponse>;
38
45
  /**
39
46
  * Checks the status of an existing charge
40
47
  */
41
- getCharge(paymentHash: string): Promise<ChargeStatus>;
48
+ getCharge(paymentHash: string, txHash?: string): Promise<ChargeStatus>;
42
49
  /**
43
50
  * Triggers a manual withdrawal of your merchant balance
44
51
  */
45
52
  payout(): Promise<PayoutResponse>;
46
53
  }
47
54
 
48
- export { Aipp, type AippConfig, type AippErrorResponse, type ChargeParams, type ChargeResponse, type ChargeStatus, type PayoutResponse };
55
+ interface L402Options {
56
+ client: Aipp;
57
+ jwtSecret: string;
58
+ resourceId: string;
59
+ amountSats?: number;
60
+ amountUsd?: number;
61
+ expiresInSeconds?: number;
62
+ }
63
+ declare function l402Paywall(options: L402Options): (req: any, res: any, next: any) => Promise<any>;
64
+ interface X402Options {
65
+ client: Aipp;
66
+ resourceId: string;
67
+ amountUsd: number;
68
+ }
69
+ declare function x402Paywall(options: X402Options): (req: any, res: any, next: any) => Promise<any>;
70
+
71
+ export { Aipp, type AippConfig, type AippErrorResponse, type ChargeParams, type ChargeResponse, type ChargeStatus, type L402Options, type PayoutResponse, type X402Options, l402Paywall, x402Paywall };
package/dist/index.d.ts CHANGED
@@ -6,16 +6,22 @@ interface ChargeParams {
6
6
  amountSats?: number;
7
7
  amountUsd?: number;
8
8
  memo?: string;
9
+ protocol?: 'L402' | 'x402';
9
10
  }
10
11
  interface ChargeResponse {
11
- payment_request: string;
12
12
  payment_hash: string;
13
- amount_sats: number;
13
+ protocol: 'L402' | 'x402';
14
+ amount_usd?: number;
15
+ pay_to?: string;
16
+ network?: string;
17
+ token?: string;
18
+ payment_request?: string;
19
+ amount_sats?: number;
14
20
  }
15
21
  interface ChargeStatus {
22
+ paid: boolean;
16
23
  status: 'pending' | 'settled';
17
- payment_hash: string;
18
- amount_sats: number;
24
+ preimage: string | null;
19
25
  }
20
26
  interface AippErrorResponse {
21
27
  error: string;
@@ -24,6 +30,7 @@ interface AippErrorResponse {
24
30
  interface PayoutResponse {
25
31
  message: string;
26
32
  amount_sats?: number;
33
+ amount_usd?: number;
27
34
  }
28
35
 
29
36
  declare class Aipp {
@@ -32,17 +39,33 @@ declare class Aipp {
32
39
  constructor(config: AippConfig);
33
40
  private request;
34
41
  /**
35
- * Creates a new Lightning Invoice
42
+ * Creates a new Invoice (either L402 or x402)
36
43
  */
37
44
  createCharge(params: ChargeParams): Promise<ChargeResponse>;
38
45
  /**
39
46
  * Checks the status of an existing charge
40
47
  */
41
- getCharge(paymentHash: string): Promise<ChargeStatus>;
48
+ getCharge(paymentHash: string, txHash?: string): Promise<ChargeStatus>;
42
49
  /**
43
50
  * Triggers a manual withdrawal of your merchant balance
44
51
  */
45
52
  payout(): Promise<PayoutResponse>;
46
53
  }
47
54
 
48
- export { Aipp, type AippConfig, type AippErrorResponse, type ChargeParams, type ChargeResponse, type ChargeStatus, type PayoutResponse };
55
+ interface L402Options {
56
+ client: Aipp;
57
+ jwtSecret: string;
58
+ resourceId: string;
59
+ amountSats?: number;
60
+ amountUsd?: number;
61
+ expiresInSeconds?: number;
62
+ }
63
+ declare function l402Paywall(options: L402Options): (req: any, res: any, next: any) => Promise<any>;
64
+ interface X402Options {
65
+ client: Aipp;
66
+ resourceId: string;
67
+ amountUsd: number;
68
+ }
69
+ declare function x402Paywall(options: X402Options): (req: any, res: any, next: any) => Promise<any>;
70
+
71
+ export { Aipp, type AippConfig, type AippErrorResponse, type ChargeParams, type ChargeResponse, type ChargeStatus, type L402Options, type PayoutResponse, type X402Options, l402Paywall, x402Paywall };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,12 +17,22 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
21
31
  var index_exports = {};
22
32
  __export(index_exports, {
23
- Aipp: () => Aipp
33
+ Aipp: () => Aipp,
34
+ l402Paywall: () => l402Paywall,
35
+ x402Paywall: () => x402Paywall
24
36
  });
25
37
  module.exports = __toCommonJS(index_exports);
26
38
 
@@ -55,7 +67,7 @@ var Aipp = class {
55
67
  return response.json();
56
68
  }
57
69
  /**
58
- * Creates a new Lightning Invoice
70
+ * Creates a new Invoice (either L402 or x402)
59
71
  */
60
72
  async createCharge(params) {
61
73
  if (!params.amountSats && !params.amountUsd) {
@@ -64,6 +76,7 @@ var Aipp = class {
64
76
  const body = { memo: params.memo };
65
77
  if (params.amountSats) body.amount_sats = params.amountSats;
66
78
  if (params.amountUsd) body.amount_usd = params.amountUsd;
79
+ if (params.protocol) body.protocol = params.protocol;
67
80
  return this.request("/invoice/create", {
68
81
  method: "POST",
69
82
  body: JSON.stringify(body)
@@ -72,11 +85,12 @@ var Aipp = class {
72
85
  /**
73
86
  * Checks the status of an existing charge
74
87
  */
75
- async getCharge(paymentHash) {
88
+ async getCharge(paymentHash, txHash) {
76
89
  if (!paymentHash) {
77
90
  throw new Error("AIPP: paymentHash is required");
78
91
  }
79
- return this.request(`/invoice/status/${paymentHash}`, {
92
+ const query = txHash ? `?tx_hash=${txHash}` : "";
93
+ return this.request(`/invoice/status/${paymentHash}${query}`, {
80
94
  method: "GET"
81
95
  });
82
96
  }
@@ -89,7 +103,163 @@ var Aipp = class {
89
103
  });
90
104
  }
91
105
  };
106
+
107
+ // src/middleware.ts
108
+ var import_crypto2 = __toESM(require("crypto"));
109
+
110
+ // src/jwt.ts
111
+ var import_crypto = __toESM(require("crypto"));
112
+ function base64urlEncode(str) {
113
+ const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
114
+ return buf.toString("base64url");
115
+ }
116
+ function base64urlDecode(str) {
117
+ return Buffer.from(str, "base64url").toString("utf8");
118
+ }
119
+ function signJwt(payload, secret) {
120
+ const header = { alg: "HS256", typ: "JWT" };
121
+ const encodedHeader = base64urlEncode(JSON.stringify(header));
122
+ const encodedPayload = base64urlEncode(JSON.stringify(payload));
123
+ const data = `${encodedHeader}.${encodedPayload}`;
124
+ const signature = import_crypto.default.createHmac("sha256", secret).update(data).digest("base64url");
125
+ return `${data}.${signature}`;
126
+ }
127
+ function verifyJwt(token, secret) {
128
+ const parts = token.split(".");
129
+ if (parts.length !== 3) {
130
+ throw new Error("Invalid JWT format");
131
+ }
132
+ const [encodedHeader, encodedPayload, signature] = parts;
133
+ const data = `${encodedHeader}.${encodedPayload}`;
134
+ const expectedSignature = import_crypto.default.createHmac("sha256", secret).update(data).digest("base64url");
135
+ const signatureBuffer = Buffer.from(signature);
136
+ const expectedBuffer = Buffer.from(expectedSignature);
137
+ if (signatureBuffer.length !== expectedBuffer.length || !import_crypto.default.timingSafeEqual(signatureBuffer, expectedBuffer)) {
138
+ throw new Error("Invalid JWT signature");
139
+ }
140
+ const payload = JSON.parse(base64urlDecode(encodedPayload));
141
+ if (payload.exp && Date.now() >= payload.exp * 1e3) {
142
+ throw new Error("JWT expired");
143
+ }
144
+ return payload;
145
+ }
146
+
147
+ // src/middleware.ts
148
+ function l402Paywall(options) {
149
+ if (!options.amountSats && !options.amountUsd) {
150
+ throw new Error("Either amountSats or amountUsd must be provided for l402Paywall");
151
+ }
152
+ return async (req, res, next) => {
153
+ const authHeader = req.headers.authorization || req.headers.Authorization;
154
+ let valid = false;
155
+ if (authHeader && typeof authHeader === "string" && authHeader.startsWith("L402 ")) {
156
+ const parts = authHeader.substring(5).split(":");
157
+ if (parts.length === 2) {
158
+ const [macaroonStr, preimage] = parts;
159
+ try {
160
+ const payload = verifyJwt(macaroonStr, options.jwtSecret);
161
+ if (payload.resource_id !== options.resourceId) {
162
+ throw new Error("Invalid resource");
163
+ }
164
+ const preimageHash = import_crypto2.default.createHash("sha256").update(Buffer.from(preimage, "hex")).digest("hex");
165
+ const expectedHash = payload.payment_hash;
166
+ if (typeof expectedHash === "string" && expectedHash.length === preimageHash.length && import_crypto2.default.timingSafeEqual(Buffer.from(preimageHash, "hex"), Buffer.from(expectedHash, "hex"))) {
167
+ valid = true;
168
+ }
169
+ } catch (e) {
170
+ }
171
+ }
172
+ }
173
+ if (valid) {
174
+ return next();
175
+ }
176
+ try {
177
+ const charge = await options.client.createCharge({
178
+ amountSats: options.amountSats,
179
+ amountUsd: options.amountUsd,
180
+ memo: `L402 Payment for ${options.resourceId}`
181
+ });
182
+ const expiresIn = options.expiresInSeconds || 3600;
183
+ const payload = {
184
+ payment_hash: charge.payment_hash,
185
+ resource_id: options.resourceId,
186
+ exp: Math.floor(Date.now() / 1e3) + expiresIn
187
+ };
188
+ const jwtToken = signJwt(payload, options.jwtSecret);
189
+ res.status(402);
190
+ res.setHeader("Www-Authenticate", `L402 macaroon="${jwtToken}" invoice="${charge.payment_request}"`);
191
+ res.json({
192
+ error: "Payment Required",
193
+ code: "L402",
194
+ payment_request: charge.payment_request,
195
+ macaroon: jwtToken
196
+ });
197
+ } catch (err) {
198
+ res.status(500).json({ error: "Failed to generate L402 challenge", details: err.message || err });
199
+ }
200
+ };
201
+ }
202
+ function x402Paywall(options) {
203
+ return async (req, res, next) => {
204
+ const authHeader = req.headers.authorization || req.headers.Authorization;
205
+ let txHash = req.query.tx_hash || req.headers["payment-signature"] || req.headers["x-payment-signature"];
206
+ let paymentHash = req.query.payment_hash || req.headers["x-payment-hash"];
207
+ if (!txHash && authHeader && typeof authHeader === "string") {
208
+ if (authHeader.startsWith("Bearer ")) {
209
+ txHash = authHeader.substring(7).trim();
210
+ } else if (authHeader.startsWith("x402 ")) {
211
+ txHash = authHeader.substring(5).trim();
212
+ } else if (authHeader.startsWith("L402 ")) {
213
+ const parts = authHeader.substring(5).split(":");
214
+ if (parts.length === 2) {
215
+ paymentHash = parts[0];
216
+ txHash = parts[1];
217
+ }
218
+ }
219
+ }
220
+ if (paymentHash && txHash) {
221
+ try {
222
+ const chargeStatus = await options.client.getCharge(paymentHash, txHash);
223
+ if (chargeStatus.status === "settled") {
224
+ return next();
225
+ }
226
+ } catch (e) {
227
+ }
228
+ }
229
+ try {
230
+ const charge = await options.client.createCharge({
231
+ amountUsd: options.amountUsd,
232
+ protocol: "x402",
233
+ memo: `x402 Payment for ${options.resourceId}`
234
+ });
235
+ const challengeObj = {
236
+ scheme: "exact",
237
+ network: "base",
238
+ payTo: charge.pay_to || "",
239
+ price: options.amountUsd.toFixed(2),
240
+ token: charge.token || "",
241
+ payment_hash: charge.payment_hash
242
+ };
243
+ const challengeBase64 = Buffer.from(JSON.stringify(challengeObj), "utf8").toString("base64");
244
+ res.setHeader("PAYMENT-REQUIRED", challengeBase64);
245
+ res.status(402);
246
+ res.json({
247
+ error: "Payment Required",
248
+ code: "x402",
249
+ payment_hash: charge.payment_hash,
250
+ pay_to: challengeObj.payTo,
251
+ price: challengeObj.price,
252
+ token: challengeObj.token,
253
+ network: challengeObj.network
254
+ });
255
+ } catch (err) {
256
+ res.status(500).json({ error: "Failed to generate x402 challenge", details: err.message || err });
257
+ }
258
+ };
259
+ }
92
260
  // Annotate the CommonJS export names for ESM import in node:
93
261
  0 && (module.exports = {
94
- Aipp
262
+ Aipp,
263
+ l402Paywall,
264
+ x402Paywall
95
265
  });
package/dist/index.mjs CHANGED
@@ -29,7 +29,7 @@ var Aipp = class {
29
29
  return response.json();
30
30
  }
31
31
  /**
32
- * Creates a new Lightning Invoice
32
+ * Creates a new Invoice (either L402 or x402)
33
33
  */
34
34
  async createCharge(params) {
35
35
  if (!params.amountSats && !params.amountUsd) {
@@ -38,6 +38,7 @@ var Aipp = class {
38
38
  const body = { memo: params.memo };
39
39
  if (params.amountSats) body.amount_sats = params.amountSats;
40
40
  if (params.amountUsd) body.amount_usd = params.amountUsd;
41
+ if (params.protocol) body.protocol = params.protocol;
41
42
  return this.request("/invoice/create", {
42
43
  method: "POST",
43
44
  body: JSON.stringify(body)
@@ -46,11 +47,12 @@ var Aipp = class {
46
47
  /**
47
48
  * Checks the status of an existing charge
48
49
  */
49
- async getCharge(paymentHash) {
50
+ async getCharge(paymentHash, txHash) {
50
51
  if (!paymentHash) {
51
52
  throw new Error("AIPP: paymentHash is required");
52
53
  }
53
- return this.request(`/invoice/status/${paymentHash}`, {
54
+ const query = txHash ? `?tx_hash=${txHash}` : "";
55
+ return this.request(`/invoice/status/${paymentHash}${query}`, {
54
56
  method: "GET"
55
57
  });
56
58
  }
@@ -63,6 +65,162 @@ var Aipp = class {
63
65
  });
64
66
  }
65
67
  };
68
+
69
+ // src/middleware.ts
70
+ import crypto2 from "crypto";
71
+
72
+ // src/jwt.ts
73
+ import crypto from "crypto";
74
+ function base64urlEncode(str) {
75
+ const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
76
+ return buf.toString("base64url");
77
+ }
78
+ function base64urlDecode(str) {
79
+ return Buffer.from(str, "base64url").toString("utf8");
80
+ }
81
+ function signJwt(payload, secret) {
82
+ const header = { alg: "HS256", typ: "JWT" };
83
+ const encodedHeader = base64urlEncode(JSON.stringify(header));
84
+ const encodedPayload = base64urlEncode(JSON.stringify(payload));
85
+ const data = `${encodedHeader}.${encodedPayload}`;
86
+ const signature = crypto.createHmac("sha256", secret).update(data).digest("base64url");
87
+ return `${data}.${signature}`;
88
+ }
89
+ function verifyJwt(token, secret) {
90
+ const parts = token.split(".");
91
+ if (parts.length !== 3) {
92
+ throw new Error("Invalid JWT format");
93
+ }
94
+ const [encodedHeader, encodedPayload, signature] = parts;
95
+ const data = `${encodedHeader}.${encodedPayload}`;
96
+ const expectedSignature = crypto.createHmac("sha256", secret).update(data).digest("base64url");
97
+ const signatureBuffer = Buffer.from(signature);
98
+ const expectedBuffer = Buffer.from(expectedSignature);
99
+ if (signatureBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
100
+ throw new Error("Invalid JWT signature");
101
+ }
102
+ const payload = JSON.parse(base64urlDecode(encodedPayload));
103
+ if (payload.exp && Date.now() >= payload.exp * 1e3) {
104
+ throw new Error("JWT expired");
105
+ }
106
+ return payload;
107
+ }
108
+
109
+ // src/middleware.ts
110
+ function l402Paywall(options) {
111
+ if (!options.amountSats && !options.amountUsd) {
112
+ throw new Error("Either amountSats or amountUsd must be provided for l402Paywall");
113
+ }
114
+ return async (req, res, next) => {
115
+ const authHeader = req.headers.authorization || req.headers.Authorization;
116
+ let valid = false;
117
+ if (authHeader && typeof authHeader === "string" && authHeader.startsWith("L402 ")) {
118
+ const parts = authHeader.substring(5).split(":");
119
+ if (parts.length === 2) {
120
+ const [macaroonStr, preimage] = parts;
121
+ try {
122
+ const payload = verifyJwt(macaroonStr, options.jwtSecret);
123
+ if (payload.resource_id !== options.resourceId) {
124
+ throw new Error("Invalid resource");
125
+ }
126
+ const preimageHash = crypto2.createHash("sha256").update(Buffer.from(preimage, "hex")).digest("hex");
127
+ const expectedHash = payload.payment_hash;
128
+ if (typeof expectedHash === "string" && expectedHash.length === preimageHash.length && crypto2.timingSafeEqual(Buffer.from(preimageHash, "hex"), Buffer.from(expectedHash, "hex"))) {
129
+ valid = true;
130
+ }
131
+ } catch (e) {
132
+ }
133
+ }
134
+ }
135
+ if (valid) {
136
+ return next();
137
+ }
138
+ try {
139
+ const charge = await options.client.createCharge({
140
+ amountSats: options.amountSats,
141
+ amountUsd: options.amountUsd,
142
+ memo: `L402 Payment for ${options.resourceId}`
143
+ });
144
+ const expiresIn = options.expiresInSeconds || 3600;
145
+ const payload = {
146
+ payment_hash: charge.payment_hash,
147
+ resource_id: options.resourceId,
148
+ exp: Math.floor(Date.now() / 1e3) + expiresIn
149
+ };
150
+ const jwtToken = signJwt(payload, options.jwtSecret);
151
+ res.status(402);
152
+ res.setHeader("Www-Authenticate", `L402 macaroon="${jwtToken}" invoice="${charge.payment_request}"`);
153
+ res.json({
154
+ error: "Payment Required",
155
+ code: "L402",
156
+ payment_request: charge.payment_request,
157
+ macaroon: jwtToken
158
+ });
159
+ } catch (err) {
160
+ res.status(500).json({ error: "Failed to generate L402 challenge", details: err.message || err });
161
+ }
162
+ };
163
+ }
164
+ function x402Paywall(options) {
165
+ return async (req, res, next) => {
166
+ const authHeader = req.headers.authorization || req.headers.Authorization;
167
+ let txHash = req.query.tx_hash || req.headers["payment-signature"] || req.headers["x-payment-signature"];
168
+ let paymentHash = req.query.payment_hash || req.headers["x-payment-hash"];
169
+ if (!txHash && authHeader && typeof authHeader === "string") {
170
+ if (authHeader.startsWith("Bearer ")) {
171
+ txHash = authHeader.substring(7).trim();
172
+ } else if (authHeader.startsWith("x402 ")) {
173
+ txHash = authHeader.substring(5).trim();
174
+ } else if (authHeader.startsWith("L402 ")) {
175
+ const parts = authHeader.substring(5).split(":");
176
+ if (parts.length === 2) {
177
+ paymentHash = parts[0];
178
+ txHash = parts[1];
179
+ }
180
+ }
181
+ }
182
+ if (paymentHash && txHash) {
183
+ try {
184
+ const chargeStatus = await options.client.getCharge(paymentHash, txHash);
185
+ if (chargeStatus.status === "settled") {
186
+ return next();
187
+ }
188
+ } catch (e) {
189
+ }
190
+ }
191
+ try {
192
+ const charge = await options.client.createCharge({
193
+ amountUsd: options.amountUsd,
194
+ protocol: "x402",
195
+ memo: `x402 Payment for ${options.resourceId}`
196
+ });
197
+ const challengeObj = {
198
+ scheme: "exact",
199
+ network: "base",
200
+ payTo: charge.pay_to || "",
201
+ price: options.amountUsd.toFixed(2),
202
+ token: charge.token || "",
203
+ payment_hash: charge.payment_hash
204
+ };
205
+ const challengeBase64 = Buffer.from(JSON.stringify(challengeObj), "utf8").toString("base64");
206
+ res.setHeader("PAYMENT-REQUIRED", challengeBase64);
207
+ res.status(402);
208
+ res.json({
209
+ error: "Payment Required",
210
+ code: "x402",
211
+ payment_hash: charge.payment_hash,
212
+ pay_to: challengeObj.payTo,
213
+ price: challengeObj.price,
214
+ token: challengeObj.token,
215
+ network: challengeObj.network
216
+ });
217
+ } catch (err) {
218
+ res.status(500).json({ error: "Failed to generate x402 challenge", details: err.message || err });
219
+ }
220
+ };
221
+ }
66
222
  export {
67
- Aipp
223
+ Aipp,
224
+ l402Paywall,
225
+ x402Paywall
68
226
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aipp-node",
3
- "version": "1.0.2",
3
+ "version": "1.2.0",
4
4
  "description": "Official Node.js SDK for AIPP - The Lightning Network Split-Payment Gateway",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/client.ts CHANGED
@@ -36,7 +36,7 @@ export class Aipp {
36
36
  }
37
37
 
38
38
  /**
39
- * Creates a new Lightning Invoice
39
+ * Creates a new Invoice (either L402 or x402)
40
40
  */
41
41
  async createCharge(params: ChargeParams): Promise<ChargeResponse> {
42
42
  if (!params.amountSats && !params.amountUsd) {
@@ -46,6 +46,7 @@ export class Aipp {
46
46
  const body: any = { memo: params.memo };
47
47
  if (params.amountSats) body.amount_sats = params.amountSats;
48
48
  if (params.amountUsd) body.amount_usd = params.amountUsd;
49
+ if (params.protocol) body.protocol = params.protocol;
49
50
 
50
51
  return this.request<ChargeResponse>('/invoice/create', {
51
52
  method: 'POST',
@@ -56,11 +57,12 @@ export class Aipp {
56
57
  /**
57
58
  * Checks the status of an existing charge
58
59
  */
59
- async getCharge(paymentHash: string): Promise<ChargeStatus> {
60
+ async getCharge(paymentHash: string, txHash?: string): Promise<ChargeStatus> {
60
61
  if (!paymentHash) {
61
62
  throw new Error('AIPP: paymentHash is required');
62
63
  }
63
- return this.request<ChargeStatus>(`/invoice/status/${paymentHash}`, {
64
+ const query = txHash ? `?tx_hash=${txHash}` : '';
65
+ return this.request<ChargeStatus>(`/invoice/status/${paymentHash}${query}`, {
64
66
  method: 'GET',
65
67
  });
66
68
  }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './client';
2
2
  export * from './types';
3
+ export * from './middleware';
package/src/jwt.ts ADDED
@@ -0,0 +1,48 @@
1
+ import crypto from 'crypto';
2
+
3
+ function base64urlEncode(str: string | Buffer): string {
4
+ const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
5
+ return buf.toString('base64url');
6
+ }
7
+
8
+ function base64urlDecode(str: string): string {
9
+ return Buffer.from(str, 'base64url').toString('utf8');
10
+ }
11
+
12
+ export function signJwt(payload: any, secret: string): string {
13
+ const header = { alg: 'HS256', typ: 'JWT' };
14
+ const encodedHeader = base64urlEncode(JSON.stringify(header));
15
+ const encodedPayload = base64urlEncode(JSON.stringify(payload));
16
+ const data = `${encodedHeader}.${encodedPayload}`;
17
+
18
+ const signature = crypto.createHmac('sha256', secret).update(data).digest('base64url');
19
+ return `${data}.${signature}`;
20
+ }
21
+
22
+ export function verifyJwt(token: string, secret: string): any {
23
+ const parts = token.split('.');
24
+ if (parts.length !== 3) {
25
+ throw new Error('Invalid JWT format');
26
+ }
27
+
28
+ const [encodedHeader, encodedPayload, signature] = parts;
29
+ const data = `${encodedHeader}.${encodedPayload}`;
30
+
31
+ const expectedSignature = crypto.createHmac('sha256', secret).update(data).digest('base64url');
32
+
33
+ // Constant time comparison to prevent timing attacks
34
+ const signatureBuffer = Buffer.from(signature);
35
+ const expectedBuffer = Buffer.from(expectedSignature);
36
+
37
+ if (signatureBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
38
+ throw new Error('Invalid JWT signature');
39
+ }
40
+
41
+ const payload = JSON.parse(base64urlDecode(encodedPayload));
42
+
43
+ if (payload.exp && Date.now() >= payload.exp * 1000) {
44
+ throw new Error('JWT expired');
45
+ }
46
+
47
+ return payload;
48
+ }
@@ -0,0 +1,159 @@
1
+ import crypto from 'crypto';
2
+ import { Aipp } from './client';
3
+ import { signJwt, verifyJwt } from './jwt';
4
+
5
+ export interface L402Options {
6
+ client: Aipp;
7
+ jwtSecret: string;
8
+ resourceId: string;
9
+ amountSats?: number;
10
+ amountUsd?: number;
11
+ expiresInSeconds?: number;
12
+ }
13
+
14
+ export function l402Paywall(options: L402Options) {
15
+ if (!options.amountSats && !options.amountUsd) {
16
+ throw new Error('Either amountSats or amountUsd must be provided for l402Paywall');
17
+ }
18
+
19
+ return async (req: any, res: any, next: any) => {
20
+ const authHeader = req.headers.authorization || req.headers.Authorization;
21
+ let valid = false;
22
+
23
+ if (authHeader && typeof authHeader === 'string' && authHeader.startsWith('L402 ')) {
24
+ const parts = authHeader.substring(5).split(':');
25
+ if (parts.length === 2) {
26
+ const [macaroonStr, preimage] = parts;
27
+ try {
28
+ // Verify JWT signature and expiration
29
+ const payload = verifyJwt(macaroonStr, options.jwtSecret);
30
+
31
+ if (payload.resource_id !== options.resourceId) {
32
+ throw new Error('Invalid resource');
33
+ }
34
+
35
+ // Verify preimage hash matches payment_hash securely
36
+ const preimageHash = crypto.createHash('sha256').update(Buffer.from(preimage, 'hex')).digest('hex');
37
+ const expectedHash = payload.payment_hash;
38
+
39
+ if (
40
+ typeof expectedHash === 'string' &&
41
+ expectedHash.length === preimageHash.length &&
42
+ crypto.timingSafeEqual(Buffer.from(preimageHash, 'hex'), Buffer.from(expectedHash, 'hex'))
43
+ ) {
44
+ valid = true;
45
+ }
46
+ } catch (e) {
47
+ // Invalid or expired JWT -> Fall through to 402 and issue new invoice
48
+ }
49
+ }
50
+ }
51
+
52
+ if (valid) {
53
+ return next();
54
+ }
55
+
56
+ try {
57
+ // Create invoice via AIPP
58
+ const charge = await options.client.createCharge({
59
+ amountSats: options.amountSats,
60
+ amountUsd: options.amountUsd,
61
+ memo: `L402 Payment for ${options.resourceId}`
62
+ });
63
+
64
+ // Issue JWT
65
+ const expiresIn = options.expiresInSeconds || 3600; // default 1 hour
66
+ const payload = {
67
+ payment_hash: charge.payment_hash,
68
+ resource_id: options.resourceId,
69
+ exp: Math.floor(Date.now() / 1000) + expiresIn
70
+ };
71
+
72
+ const jwtToken = signJwt(payload, options.jwtSecret);
73
+
74
+ res.status(402);
75
+ res.setHeader('Www-Authenticate', `L402 macaroon="${jwtToken}" invoice="${charge.payment_request}"`);
76
+ res.json({
77
+ error: "Payment Required",
78
+ code: "L402",
79
+ payment_request: charge.payment_request,
80
+ macaroon: jwtToken
81
+ });
82
+ } catch (err: any) {
83
+ res.status(500).json({ error: "Failed to generate L402 challenge", details: err.message || err });
84
+ }
85
+ };
86
+ }
87
+
88
+ export interface X402Options {
89
+ client: Aipp;
90
+ resourceId: string;
91
+ amountUsd: number;
92
+ }
93
+
94
+ export function x402Paywall(options: X402Options) {
95
+ return async (req: any, res: any, next: any) => {
96
+ const authHeader = req.headers.authorization || req.headers.Authorization;
97
+ let txHash = (req.query.tx_hash || req.headers['payment-signature'] || req.headers['x-payment-signature']) as string;
98
+ let paymentHash = (req.query.payment_hash || req.headers['x-payment-hash']) as string;
99
+
100
+ if (!txHash && authHeader && typeof authHeader === 'string') {
101
+ if (authHeader.startsWith('Bearer ')) {
102
+ txHash = authHeader.substring(7).trim();
103
+ } else if (authHeader.startsWith('x402 ')) {
104
+ txHash = authHeader.substring(5).trim();
105
+ } else if (authHeader.startsWith('L402 ')) {
106
+ // Fallback or double format check
107
+ const parts = authHeader.substring(5).split(':');
108
+ if (parts.length === 2) {
109
+ paymentHash = parts[0];
110
+ txHash = parts[1];
111
+ }
112
+ }
113
+ }
114
+
115
+ if (paymentHash && txHash) {
116
+ try {
117
+ const chargeStatus = await options.client.getCharge(paymentHash, txHash);
118
+ if (chargeStatus.status === 'settled') {
119
+ return next();
120
+ }
121
+ } catch (e) {
122
+ // Validation failed, proceed to challenge
123
+ }
124
+ }
125
+
126
+ try {
127
+ const charge = await options.client.createCharge({
128
+ amountUsd: options.amountUsd,
129
+ protocol: 'x402',
130
+ memo: `x402 Payment for ${options.resourceId}`
131
+ });
132
+
133
+ const challengeObj = {
134
+ scheme: 'exact',
135
+ network: 'base',
136
+ payTo: charge.pay_to || '',
137
+ price: options.amountUsd.toFixed(2),
138
+ token: charge.token || '',
139
+ payment_hash: charge.payment_hash
140
+ };
141
+
142
+ const challengeBase64 = Buffer.from(JSON.stringify(challengeObj), 'utf8').toString('base64');
143
+ res.setHeader('PAYMENT-REQUIRED', challengeBase64);
144
+
145
+ res.status(402);
146
+ res.json({
147
+ error: "Payment Required",
148
+ code: "x402",
149
+ payment_hash: charge.payment_hash,
150
+ pay_to: challengeObj.payTo,
151
+ price: challengeObj.price,
152
+ token: challengeObj.token,
153
+ network: challengeObj.network
154
+ });
155
+ } catch (err: any) {
156
+ res.status(500).json({ error: "Failed to generate x402 challenge", details: err.message || err });
157
+ }
158
+ };
159
+ }
package/src/types.ts CHANGED
@@ -7,18 +7,24 @@ export interface ChargeParams {
7
7
  amountSats?: number;
8
8
  amountUsd?: number;
9
9
  memo?: string;
10
+ protocol?: 'L402' | 'x402';
10
11
  }
11
12
 
12
13
  export interface ChargeResponse {
13
- payment_request: string;
14
14
  payment_hash: string;
15
- amount_sats: number;
15
+ protocol: 'L402' | 'x402';
16
+ amount_usd?: number;
17
+ pay_to?: string;
18
+ network?: string;
19
+ token?: string;
20
+ payment_request?: string; // For L402
21
+ amount_sats?: number; // For L402
16
22
  }
17
23
 
18
24
  export interface ChargeStatus {
25
+ paid: boolean;
19
26
  status: 'pending' | 'settled';
20
- payment_hash: string;
21
- amount_sats: number;
27
+ preimage: string | null;
22
28
  }
23
29
 
24
30
  export interface AippErrorResponse {
@@ -29,4 +35,5 @@ export interface AippErrorResponse {
29
35
  export interface PayoutResponse {
30
36
  message: string;
31
37
  amount_sats?: number;
38
+ amount_usd?: number;
32
39
  }