aipp-node 1.0.2 → 1.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/dist/index.d.mts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +110 -2
- package/dist/index.mjs +98 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/jwt.ts +48 -0
- package/src/middleware.ts +86 -0
package/dist/index.d.mts
CHANGED
|
@@ -45,4 +45,14 @@ declare class Aipp {
|
|
|
45
45
|
payout(): Promise<PayoutResponse>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
interface L402Options {
|
|
49
|
+
client: Aipp;
|
|
50
|
+
jwtSecret: string;
|
|
51
|
+
resourceId: string;
|
|
52
|
+
amountSats?: number;
|
|
53
|
+
amountUsd?: number;
|
|
54
|
+
expiresInSeconds?: number;
|
|
55
|
+
}
|
|
56
|
+
declare function l402Paywall(options: L402Options): (req: any, res: any, next: any) => Promise<any>;
|
|
57
|
+
|
|
58
|
+
export { Aipp, type AippConfig, type AippErrorResponse, type ChargeParams, type ChargeResponse, type ChargeStatus, type L402Options, type PayoutResponse, l402Paywall };
|
package/dist/index.d.ts
CHANGED
|
@@ -45,4 +45,14 @@ declare class Aipp {
|
|
|
45
45
|
payout(): Promise<PayoutResponse>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
interface L402Options {
|
|
49
|
+
client: Aipp;
|
|
50
|
+
jwtSecret: string;
|
|
51
|
+
resourceId: string;
|
|
52
|
+
amountSats?: number;
|
|
53
|
+
amountUsd?: number;
|
|
54
|
+
expiresInSeconds?: number;
|
|
55
|
+
}
|
|
56
|
+
declare function l402Paywall(options: L402Options): (req: any, res: any, next: any) => Promise<any>;
|
|
57
|
+
|
|
58
|
+
export { Aipp, type AippConfig, type AippErrorResponse, type ChargeParams, type ChargeResponse, type ChargeStatus, type L402Options, type PayoutResponse, l402Paywall };
|
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,21 @@ 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
|
|
24
35
|
});
|
|
25
36
|
module.exports = __toCommonJS(index_exports);
|
|
26
37
|
|
|
@@ -89,7 +100,104 @@ var Aipp = class {
|
|
|
89
100
|
});
|
|
90
101
|
}
|
|
91
102
|
};
|
|
103
|
+
|
|
104
|
+
// src/middleware.ts
|
|
105
|
+
var import_crypto2 = __toESM(require("crypto"));
|
|
106
|
+
|
|
107
|
+
// src/jwt.ts
|
|
108
|
+
var import_crypto = __toESM(require("crypto"));
|
|
109
|
+
function base64urlEncode(str) {
|
|
110
|
+
const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
|
|
111
|
+
return buf.toString("base64url");
|
|
112
|
+
}
|
|
113
|
+
function base64urlDecode(str) {
|
|
114
|
+
return Buffer.from(str, "base64url").toString("utf8");
|
|
115
|
+
}
|
|
116
|
+
function signJwt(payload, secret) {
|
|
117
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
118
|
+
const encodedHeader = base64urlEncode(JSON.stringify(header));
|
|
119
|
+
const encodedPayload = base64urlEncode(JSON.stringify(payload));
|
|
120
|
+
const data = `${encodedHeader}.${encodedPayload}`;
|
|
121
|
+
const signature = import_crypto.default.createHmac("sha256", secret).update(data).digest("base64url");
|
|
122
|
+
return `${data}.${signature}`;
|
|
123
|
+
}
|
|
124
|
+
function verifyJwt(token, secret) {
|
|
125
|
+
const parts = token.split(".");
|
|
126
|
+
if (parts.length !== 3) {
|
|
127
|
+
throw new Error("Invalid JWT format");
|
|
128
|
+
}
|
|
129
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
130
|
+
const data = `${encodedHeader}.${encodedPayload}`;
|
|
131
|
+
const expectedSignature = import_crypto.default.createHmac("sha256", secret).update(data).digest("base64url");
|
|
132
|
+
const signatureBuffer = Buffer.from(signature);
|
|
133
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
134
|
+
if (signatureBuffer.length !== expectedBuffer.length || !import_crypto.default.timingSafeEqual(signatureBuffer, expectedBuffer)) {
|
|
135
|
+
throw new Error("Invalid JWT signature");
|
|
136
|
+
}
|
|
137
|
+
const payload = JSON.parse(base64urlDecode(encodedPayload));
|
|
138
|
+
if (payload.exp && Date.now() >= payload.exp * 1e3) {
|
|
139
|
+
throw new Error("JWT expired");
|
|
140
|
+
}
|
|
141
|
+
return payload;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/middleware.ts
|
|
145
|
+
function l402Paywall(options) {
|
|
146
|
+
if (!options.amountSats && !options.amountUsd) {
|
|
147
|
+
throw new Error("Either amountSats or amountUsd must be provided for l402Paywall");
|
|
148
|
+
}
|
|
149
|
+
return async (req, res, next) => {
|
|
150
|
+
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
151
|
+
let valid = false;
|
|
152
|
+
if (authHeader && typeof authHeader === "string" && authHeader.startsWith("L402 ")) {
|
|
153
|
+
const parts = authHeader.substring(5).split(":");
|
|
154
|
+
if (parts.length === 2) {
|
|
155
|
+
const [macaroonStr, preimage] = parts;
|
|
156
|
+
try {
|
|
157
|
+
const payload = verifyJwt(macaroonStr, options.jwtSecret);
|
|
158
|
+
if (payload.resource_id !== options.resourceId) {
|
|
159
|
+
throw new Error("Invalid resource");
|
|
160
|
+
}
|
|
161
|
+
const preimageHash = import_crypto2.default.createHash("sha256").update(Buffer.from(preimage, "hex")).digest("hex");
|
|
162
|
+
const expectedHash = payload.payment_hash;
|
|
163
|
+
if (typeof expectedHash === "string" && expectedHash.length === preimageHash.length && import_crypto2.default.timingSafeEqual(Buffer.from(preimageHash, "hex"), Buffer.from(expectedHash, "hex"))) {
|
|
164
|
+
valid = true;
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (valid) {
|
|
171
|
+
return next();
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const charge = await options.client.createCharge({
|
|
175
|
+
amountSats: options.amountSats,
|
|
176
|
+
amountUsd: options.amountUsd,
|
|
177
|
+
memo: `L402 Payment for ${options.resourceId}`
|
|
178
|
+
});
|
|
179
|
+
const expiresIn = options.expiresInSeconds || 3600;
|
|
180
|
+
const payload = {
|
|
181
|
+
payment_hash: charge.payment_hash,
|
|
182
|
+
resource_id: options.resourceId,
|
|
183
|
+
exp: Math.floor(Date.now() / 1e3) + expiresIn
|
|
184
|
+
};
|
|
185
|
+
const jwtToken = signJwt(payload, options.jwtSecret);
|
|
186
|
+
res.status(402);
|
|
187
|
+
res.setHeader("Www-Authenticate", `L402 macaroon="${jwtToken}" invoice="${charge.payment_request}"`);
|
|
188
|
+
res.json({
|
|
189
|
+
error: "Payment Required",
|
|
190
|
+
code: "L402",
|
|
191
|
+
payment_request: charge.payment_request,
|
|
192
|
+
macaroon: jwtToken
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
res.status(500).json({ error: "Failed to generate L402 challenge", details: err.message || err });
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
92
199
|
// Annotate the CommonJS export names for ESM import in node:
|
|
93
200
|
0 && (module.exports = {
|
|
94
|
-
Aipp
|
|
201
|
+
Aipp,
|
|
202
|
+
l402Paywall
|
|
95
203
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -63,6 +63,103 @@ var Aipp = class {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
+
|
|
67
|
+
// src/middleware.ts
|
|
68
|
+
import crypto2 from "crypto";
|
|
69
|
+
|
|
70
|
+
// src/jwt.ts
|
|
71
|
+
import crypto from "crypto";
|
|
72
|
+
function base64urlEncode(str) {
|
|
73
|
+
const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
|
|
74
|
+
return buf.toString("base64url");
|
|
75
|
+
}
|
|
76
|
+
function base64urlDecode(str) {
|
|
77
|
+
return Buffer.from(str, "base64url").toString("utf8");
|
|
78
|
+
}
|
|
79
|
+
function signJwt(payload, secret) {
|
|
80
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
81
|
+
const encodedHeader = base64urlEncode(JSON.stringify(header));
|
|
82
|
+
const encodedPayload = base64urlEncode(JSON.stringify(payload));
|
|
83
|
+
const data = `${encodedHeader}.${encodedPayload}`;
|
|
84
|
+
const signature = crypto.createHmac("sha256", secret).update(data).digest("base64url");
|
|
85
|
+
return `${data}.${signature}`;
|
|
86
|
+
}
|
|
87
|
+
function verifyJwt(token, secret) {
|
|
88
|
+
const parts = token.split(".");
|
|
89
|
+
if (parts.length !== 3) {
|
|
90
|
+
throw new Error("Invalid JWT format");
|
|
91
|
+
}
|
|
92
|
+
const [encodedHeader, encodedPayload, signature] = parts;
|
|
93
|
+
const data = `${encodedHeader}.${encodedPayload}`;
|
|
94
|
+
const expectedSignature = crypto.createHmac("sha256", secret).update(data).digest("base64url");
|
|
95
|
+
const signatureBuffer = Buffer.from(signature);
|
|
96
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
97
|
+
if (signatureBuffer.length !== expectedBuffer.length || !crypto.timingSafeEqual(signatureBuffer, expectedBuffer)) {
|
|
98
|
+
throw new Error("Invalid JWT signature");
|
|
99
|
+
}
|
|
100
|
+
const payload = JSON.parse(base64urlDecode(encodedPayload));
|
|
101
|
+
if (payload.exp && Date.now() >= payload.exp * 1e3) {
|
|
102
|
+
throw new Error("JWT expired");
|
|
103
|
+
}
|
|
104
|
+
return payload;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/middleware.ts
|
|
108
|
+
function l402Paywall(options) {
|
|
109
|
+
if (!options.amountSats && !options.amountUsd) {
|
|
110
|
+
throw new Error("Either amountSats or amountUsd must be provided for l402Paywall");
|
|
111
|
+
}
|
|
112
|
+
return async (req, res, next) => {
|
|
113
|
+
const authHeader = req.headers.authorization || req.headers.Authorization;
|
|
114
|
+
let valid = false;
|
|
115
|
+
if (authHeader && typeof authHeader === "string" && authHeader.startsWith("L402 ")) {
|
|
116
|
+
const parts = authHeader.substring(5).split(":");
|
|
117
|
+
if (parts.length === 2) {
|
|
118
|
+
const [macaroonStr, preimage] = parts;
|
|
119
|
+
try {
|
|
120
|
+
const payload = verifyJwt(macaroonStr, options.jwtSecret);
|
|
121
|
+
if (payload.resource_id !== options.resourceId) {
|
|
122
|
+
throw new Error("Invalid resource");
|
|
123
|
+
}
|
|
124
|
+
const preimageHash = crypto2.createHash("sha256").update(Buffer.from(preimage, "hex")).digest("hex");
|
|
125
|
+
const expectedHash = payload.payment_hash;
|
|
126
|
+
if (typeof expectedHash === "string" && expectedHash.length === preimageHash.length && crypto2.timingSafeEqual(Buffer.from(preimageHash, "hex"), Buffer.from(expectedHash, "hex"))) {
|
|
127
|
+
valid = true;
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (valid) {
|
|
134
|
+
return next();
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const charge = await options.client.createCharge({
|
|
138
|
+
amountSats: options.amountSats,
|
|
139
|
+
amountUsd: options.amountUsd,
|
|
140
|
+
memo: `L402 Payment for ${options.resourceId}`
|
|
141
|
+
});
|
|
142
|
+
const expiresIn = options.expiresInSeconds || 3600;
|
|
143
|
+
const payload = {
|
|
144
|
+
payment_hash: charge.payment_hash,
|
|
145
|
+
resource_id: options.resourceId,
|
|
146
|
+
exp: Math.floor(Date.now() / 1e3) + expiresIn
|
|
147
|
+
};
|
|
148
|
+
const jwtToken = signJwt(payload, options.jwtSecret);
|
|
149
|
+
res.status(402);
|
|
150
|
+
res.setHeader("Www-Authenticate", `L402 macaroon="${jwtToken}" invoice="${charge.payment_request}"`);
|
|
151
|
+
res.json({
|
|
152
|
+
error: "Payment Required",
|
|
153
|
+
code: "L402",
|
|
154
|
+
payment_request: charge.payment_request,
|
|
155
|
+
macaroon: jwtToken
|
|
156
|
+
});
|
|
157
|
+
} catch (err) {
|
|
158
|
+
res.status(500).json({ error: "Failed to generate L402 challenge", details: err.message || err });
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
66
162
|
export {
|
|
67
|
-
Aipp
|
|
163
|
+
Aipp,
|
|
164
|
+
l402Paywall
|
|
68
165
|
};
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
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,86 @@
|
|
|
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
|
+
}
|