@fiber-pay/sdk 0.1.0-rc.7 → 0.1.1
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/README.md +10 -0
- package/dist/index.d.ts +227 -1
- package/dist/index.js +395 -0
- package/dist/index.js.map +1 -1
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -48,7 +48,17 @@ console.log(facts);
|
|
|
48
48
|
This helper aligns with upstream Fiber Biscuit permission mapping (method -> read/write resource),
|
|
49
49
|
and can be used to prepare `permissions.bc` inputs before signing tokens.
|
|
50
50
|
|
|
51
|
+
## L402 Protocol
|
|
52
|
+
|
|
53
|
+
The SDK includes L402 payment-gating primitives for Express APIs:
|
|
54
|
+
|
|
55
|
+
- `MacaroonService` — mint and verify L402 tokens
|
|
56
|
+
- `createL402Middleware()` — Express middleware for 402 challenge-response flow
|
|
57
|
+
|
|
58
|
+
See [docs/l402-agent-guide.md](../../docs/l402-agent-guide.md) for usage.
|
|
59
|
+
|
|
51
60
|
## Compatibility
|
|
52
61
|
|
|
53
62
|
- Node.js `>=20`
|
|
54
63
|
- Fiber target: `v0.7.1`
|
|
64
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -1028,6 +1029,231 @@ declare class LiquidityAnalyzer {
|
|
|
1028
1029
|
}>;
|
|
1029
1030
|
}
|
|
1030
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* MacaroonService
|
|
1034
|
+
*
|
|
1035
|
+
* Handles L402 macaroon minting, verification, and caveat management.
|
|
1036
|
+
* Uses the `macaroon` npm package for cryptographic operations and
|
|
1037
|
+
* Node.js built-in `crypto` for SHA-256 hashing.
|
|
1038
|
+
*/
|
|
1039
|
+
interface MacaroonCaveat {
|
|
1040
|
+
condition: string;
|
|
1041
|
+
value: string;
|
|
1042
|
+
}
|
|
1043
|
+
interface MintParams {
|
|
1044
|
+
identifier: string;
|
|
1045
|
+
paymentHash: string;
|
|
1046
|
+
resourceId?: string;
|
|
1047
|
+
resourceType?: string;
|
|
1048
|
+
expirySeconds?: number;
|
|
1049
|
+
location?: string;
|
|
1050
|
+
}
|
|
1051
|
+
interface VerifyResult {
|
|
1052
|
+
valid: boolean;
|
|
1053
|
+
error?: string;
|
|
1054
|
+
caveats?: Record<string, string>;
|
|
1055
|
+
}
|
|
1056
|
+
declare class MacaroonService {
|
|
1057
|
+
private rootKey;
|
|
1058
|
+
constructor(rootKey?: string);
|
|
1059
|
+
/**
|
|
1060
|
+
* Mint a new macaroon with embedded caveats.
|
|
1061
|
+
*
|
|
1062
|
+
* The macaroon identifier encodes the payment hash, resource info, and
|
|
1063
|
+
* version. First-party caveats are added for payment_hash, expiry, and
|
|
1064
|
+
* optionally resource_id / resource_type.
|
|
1065
|
+
*/
|
|
1066
|
+
mint(params: MintParams): {
|
|
1067
|
+
macaroon: string;
|
|
1068
|
+
caveats: MacaroonCaveat[];
|
|
1069
|
+
};
|
|
1070
|
+
/**
|
|
1071
|
+
* Verify a macaroon with a payment preimage.
|
|
1072
|
+
*
|
|
1073
|
+
* Validates:
|
|
1074
|
+
* 1. SHA-256(preimage) === payment_hash in the macaroon identifier
|
|
1075
|
+
* 2. Macaroon signature against root key
|
|
1076
|
+
* 3. Expiry caveat is not past
|
|
1077
|
+
*/
|
|
1078
|
+
verify(macaroonB64: string, preimage: string): VerifyResult;
|
|
1079
|
+
/**
|
|
1080
|
+
* Verify macaroon signature and caveats without requiring a preimage.
|
|
1081
|
+
*
|
|
1082
|
+
* Used in the "connected-node" flow where the middleware verifies
|
|
1083
|
+
* invoice settlement via Fiber RPC instead of requiring the client
|
|
1084
|
+
* to provide the preimage directly.
|
|
1085
|
+
*/
|
|
1086
|
+
verifyWithoutPreimage(macaroonB64: string): VerifyResult;
|
|
1087
|
+
/** Extract caveats from a macaroon without full verification. */
|
|
1088
|
+
extractCaveats(macaroonB64: string): Record<string, string>;
|
|
1089
|
+
private generateRootKey;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/**
|
|
1093
|
+
* L402 Protocol Types
|
|
1094
|
+
*
|
|
1095
|
+
* Type definitions for the L402 (HTTP 402 + Macaroon + Lightning Invoice)
|
|
1096
|
+
* payment protocol. These types are framework-agnostic where possible;
|
|
1097
|
+
* Express-specific types are isolated behind optional peer dependency.
|
|
1098
|
+
*/
|
|
1099
|
+
|
|
1100
|
+
/** Fiber Network invoice used in L402 challenges. */
|
|
1101
|
+
interface L402Invoice {
|
|
1102
|
+
paymentHash: string;
|
|
1103
|
+
invoiceAddress: string;
|
|
1104
|
+
amount: string;
|
|
1105
|
+
description: string;
|
|
1106
|
+
expiry: number;
|
|
1107
|
+
createdAt: number;
|
|
1108
|
+
}
|
|
1109
|
+
/** Authorization token combining macaroon + payment preimage. */
|
|
1110
|
+
interface L402Token {
|
|
1111
|
+
macaroon: string;
|
|
1112
|
+
preimage: string;
|
|
1113
|
+
}
|
|
1114
|
+
/** Data returned in a 402 response. */
|
|
1115
|
+
interface L402Challenge {
|
|
1116
|
+
macaroon: string;
|
|
1117
|
+
invoice: string;
|
|
1118
|
+
}
|
|
1119
|
+
/** Core L402 configuration. */
|
|
1120
|
+
interface L402Config {
|
|
1121
|
+
/** Hex string for macaroon signing (32 bytes / 64 hex chars). Falls back to L402_ROOT_KEY env or a secure random key. */
|
|
1122
|
+
rootKey?: string;
|
|
1123
|
+
/** Default expiry for macaroon + invoice in seconds. Default: 3600. */
|
|
1124
|
+
expirySeconds: number;
|
|
1125
|
+
/** Default price in CKB. */
|
|
1126
|
+
priceCkb: number;
|
|
1127
|
+
}
|
|
1128
|
+
/** Express request augmented with L402 validation result. */
|
|
1129
|
+
interface L402Request extends Request {
|
|
1130
|
+
l402?: {
|
|
1131
|
+
valid: boolean;
|
|
1132
|
+
preimage?: string;
|
|
1133
|
+
paymentHash?: string;
|
|
1134
|
+
token?: L402Token;
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
/** Full middleware configuration including rate limiting. */
|
|
1138
|
+
interface L402MiddlewareConfig extends L402Config {
|
|
1139
|
+
rateLimitWindowMs: number;
|
|
1140
|
+
rateLimitMaxRequests: number;
|
|
1141
|
+
}
|
|
1142
|
+
/** In-memory challenge store interface. */
|
|
1143
|
+
interface ChallengeStore {
|
|
1144
|
+
get(key: string): L402Challenge | undefined;
|
|
1145
|
+
set(key: string, value: L402Challenge): void;
|
|
1146
|
+
delete(key: string): void;
|
|
1147
|
+
has(key: string): boolean;
|
|
1148
|
+
}
|
|
1149
|
+
/** Metadata about a protected resource (used for dynamic pricing). */
|
|
1150
|
+
interface ProtectedResourceInfo {
|
|
1151
|
+
id?: string;
|
|
1152
|
+
type?: string;
|
|
1153
|
+
priceCkb?: number;
|
|
1154
|
+
}
|
|
1155
|
+
/** Resolves request → resource info for dynamic pricing. */
|
|
1156
|
+
interface ResourceResolver {
|
|
1157
|
+
name: string;
|
|
1158
|
+
matches(req: Request): boolean;
|
|
1159
|
+
resolve(req: Request): Promise<ProtectedResourceInfo | undefined>;
|
|
1160
|
+
}
|
|
1161
|
+
/** Registry that matches requests to the appropriate resolver. */
|
|
1162
|
+
interface ResourceResolverRegistry {
|
|
1163
|
+
register(resolver: ResourceResolver): void;
|
|
1164
|
+
resolve(req: Request): Promise<ProtectedResourceInfo | undefined>;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* L402 Middleware for Express
|
|
1169
|
+
*
|
|
1170
|
+
* Protects routes with the L402 payment protocol. On unauthenticated
|
|
1171
|
+
* requests it issues a 402 challenge (macaroon + Fiber invoice); on
|
|
1172
|
+
* subsequent requests it verifies the L402 token.
|
|
1173
|
+
*
|
|
1174
|
+
* Two verification paths are supported:
|
|
1175
|
+
* Path A — client provides macaroon:preimage (legacy / manual flow)
|
|
1176
|
+
* Path B — client provides macaroon only; middleware checks invoice
|
|
1177
|
+
* settlement via Fiber RPC (connected-node flow)
|
|
1178
|
+
*
|
|
1179
|
+
* Uses `FiberRpcClient` from `@fiber-pay/sdk` directly for invoice
|
|
1180
|
+
* creation and settlement checks, eliminating the intermediate
|
|
1181
|
+
* InvoiceService abstraction from the upstream fiber-l402 SDK.
|
|
1182
|
+
*/
|
|
1183
|
+
|
|
1184
|
+
interface L402ResourceResolver {
|
|
1185
|
+
resolve(req: Request): Promise<ProtectedResourceInfo | undefined>;
|
|
1186
|
+
}
|
|
1187
|
+
type LegacyResourceProvider = (req: Request) => Promise<ProtectedResourceInfo | undefined> | ProtectedResourceInfo | undefined;
|
|
1188
|
+
declare class L402Middleware {
|
|
1189
|
+
private config;
|
|
1190
|
+
private rateLimitStore;
|
|
1191
|
+
private macaroonService;
|
|
1192
|
+
private rpcClient;
|
|
1193
|
+
private resourceResolver?;
|
|
1194
|
+
private currency;
|
|
1195
|
+
constructor(config?: Partial<L402MiddlewareConfig> & {
|
|
1196
|
+
/** Pre-configured RPC client. Takes precedence over rpcUrl. */
|
|
1197
|
+
rpcClient?: FiberRpcClient;
|
|
1198
|
+
/** Fiber node RPC URL. Used when rpcClient is not provided. */
|
|
1199
|
+
rpcUrl?: string;
|
|
1200
|
+
/** Biscuit token for Fiber RPC authentication. */
|
|
1201
|
+
biscuitToken?: string;
|
|
1202
|
+
/** Invoice currency. Default: 'Fibt' (testnet). */
|
|
1203
|
+
currency?: Currency;
|
|
1204
|
+
/** Resource resolver registry for dynamic pricing. */
|
|
1205
|
+
resourceResolver?: L402ResourceResolver;
|
|
1206
|
+
/** @deprecated Use resourceResolver instead. */
|
|
1207
|
+
resourceProvider?: LegacyResourceProvider;
|
|
1208
|
+
});
|
|
1209
|
+
handle(req: L402Request, res: Response, next: NextFunction): Promise<void>;
|
|
1210
|
+
private validateResourceCaveats;
|
|
1211
|
+
private validatePaymentHashHeader;
|
|
1212
|
+
private checkRateLimit;
|
|
1213
|
+
private issueChallenge;
|
|
1214
|
+
private createChallenge;
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Create an L402 middleware function for Express.
|
|
1218
|
+
*
|
|
1219
|
+
* @example
|
|
1220
|
+
* ```ts
|
|
1221
|
+
* import express from 'express';
|
|
1222
|
+
* import { createL402Middleware } from '@fiber-pay/sdk';
|
|
1223
|
+
*
|
|
1224
|
+
* const app = express();
|
|
1225
|
+
*
|
|
1226
|
+
* app.get('/api/premium/*', createL402Middleware({
|
|
1227
|
+
* rootKey: process.env.L402_ROOT_KEY,
|
|
1228
|
+
* priceCkb: 0.1,
|
|
1229
|
+
* expirySeconds: 3600,
|
|
1230
|
+
* }));
|
|
1231
|
+
* ```
|
|
1232
|
+
*/
|
|
1233
|
+
declare function createL402Middleware(config?: Partial<L402MiddlewareConfig> & {
|
|
1234
|
+
rpcClient?: FiberRpcClient;
|
|
1235
|
+
rpcUrl?: string;
|
|
1236
|
+
biscuitToken?: string;
|
|
1237
|
+
currency?: Currency;
|
|
1238
|
+
resourceResolver?: L402ResourceResolver;
|
|
1239
|
+
resourceProvider?: LegacyResourceProvider;
|
|
1240
|
+
}): (req: L402Request, res: Response, next: NextFunction) => Promise<void>;
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Default Resource Resolver Registry
|
|
1244
|
+
*
|
|
1245
|
+
* Iterates registered resolvers to match incoming requests to protected
|
|
1246
|
+
* resource metadata (id, type, price). Used by L402Middleware for
|
|
1247
|
+
* dynamic per-resource pricing.
|
|
1248
|
+
*/
|
|
1249
|
+
|
|
1250
|
+
declare class DefaultResourceResolverRegistry implements ResourceResolverRegistry {
|
|
1251
|
+
private resolvers;
|
|
1252
|
+
constructor(resolvers?: ResourceResolver[]);
|
|
1253
|
+
register(resolver: ResourceResolver): void;
|
|
1254
|
+
resolve(req: Request): Promise<ProtectedResourceInfo | undefined>;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1031
1257
|
/**
|
|
1032
1258
|
* Biscuit policy helpers for Fiber RPC.
|
|
1033
1259
|
*
|
|
@@ -1251,4 +1477,4 @@ declare class InvoiceVerifier {
|
|
|
1251
1477
|
private calculateTrustScore;
|
|
1252
1478
|
}
|
|
1253
1479
|
|
|
1254
|
-
export { AUTH_TAG_LENGTH, type AbandonChannelParams, type AcceptChannelParams, type AcceptChannelResult, type AgentSession, type Attribute, type AuditAction, type AuditLogEntry, type BiscuitAction, type BiscuitMethodRule, type BiscuitPermission, type BuildRouterParams, type BuildRouterResult, type CancelInvoiceParams, type CancelInvoiceResult, type CchInvoice, type CchOrderStatus, type CellOutput, type Channel, type ChannelId, type ChannelPolicy, ChannelPolicySchema, ChannelState, type ChannelStateFlags, type ChannelUpdateInfo, type CkbInvoice, type CkbInvoiceStatus, type ConnectPeerParams, type ConnectPeerResult, type Currency, DEFAULT_SECURITY_POLICY, type DisconnectPeerParams, ENCRYPTED_MAGIC, FiberRpcClient, FiberRpcError, type GetInvoiceParams, type GetInvoiceResult, type GetPaymentParams, type GetPaymentResult, type GraphChannelInfo, type GraphChannelsParams, type GraphChannelsResult, type GraphNodeInfo, type GraphNodesParams, type GraphNodesResult, type Hash256, type HashAlgorithm, type HexString, type HopHint, type HopRequire, type Htlc, IV_LENGTH, type InvoiceData, type InvoiceSignature, type InvoiceVerificationResult, InvoiceVerifier, type JsonRpcError, type JsonRpcRequest, type JsonRpcResponse, KEY_LENGTH, type KeyConfig, type KeyInfo, LiquidityAnalyzer, type LiquidityReport, type ListChannelsParams, type ListChannelsResult, type ListPeersResult, type Multiaddr, type NewInvoiceParams, type NewInvoiceResult, type NodeInfo, type NodeInfoResult, type OpenChannelParams, type OpenChannelResult, type OutPoint, type ParseInvoiceParams, type ParseInvoiceResult, type PaymentCustomRecords, type PaymentHash, type PaymentInfo, type PaymentStatus, type PeerId, type PeerInfo, type PolicyCheckResult, PolicyEngine, type PolicyViolation, type Privkey, type Pubkey, type RateLimit, RateLimitSchema, type RecipientPolicy, RecipientPolicySchema, type RemoveTlcReason, type RevocationData, type RouterHop, SALT_LENGTH, SCRYPT_N, SCRYPT_P, SCRYPT_R, type Script$1 as Script, type SecurityPolicy, SecurityPolicySchema, type SendPaymentParams, type SendPaymentResult, type SendPaymentWithRouterParams, type SessionRoute, type SessionRouteNode, type SettleInvoiceParams, type SettlementData, type SettlementTlc, type ShutdownChannelParams, type SpendingLimit, SpendingLimitSchema, type TLCId, type TlcStatus, type UdtArgInfo, type UdtCellDep, type UdtCfgInfos, type UdtDep, type UdtScript, type UpdateChannelParams, type ViolationType, buildMultiaddr, buildMultiaddrFromNodeId, buildMultiaddrFromRpcUrl, ckbHash, ckbToShannons, collectBiscuitPermissions, decryptKey, derivePublicKey, fromHex, generatePreimage, generatePrivateKey, getBiscuitRuleForMethod, hashPreimage, isEncryptedKey, listSupportedBiscuitMethods, nodeIdToPeerId, randomBytes32, renderBiscuitFactsForMethods, renderBiscuitPermissionFacts, scriptToAddress, sha256Hash, shannonsToCkb, toHex, verifyPreimageHash };
|
|
1480
|
+
export { AUTH_TAG_LENGTH, type AbandonChannelParams, type AcceptChannelParams, type AcceptChannelResult, type AgentSession, type Attribute, type AuditAction, type AuditLogEntry, type BiscuitAction, type BiscuitMethodRule, type BiscuitPermission, type BuildRouterParams, type BuildRouterResult, type CancelInvoiceParams, type CancelInvoiceResult, type CchInvoice, type CchOrderStatus, type CellOutput, type ChallengeStore, type Channel, type ChannelId, type ChannelPolicy, ChannelPolicySchema, ChannelState, type ChannelStateFlags, type ChannelUpdateInfo, type CkbInvoice, type CkbInvoiceStatus, type ConnectPeerParams, type ConnectPeerResult, type Currency, DEFAULT_SECURITY_POLICY, DefaultResourceResolverRegistry, type DisconnectPeerParams, ENCRYPTED_MAGIC, FiberRpcClient, FiberRpcError, type GetInvoiceParams, type GetInvoiceResult, type GetPaymentParams, type GetPaymentResult, type GraphChannelInfo, type GraphChannelsParams, type GraphChannelsResult, type GraphNodeInfo, type GraphNodesParams, type GraphNodesResult, type Hash256, type HashAlgorithm, type HexString, type HopHint, type HopRequire, type Htlc, IV_LENGTH, type InvoiceData, type InvoiceSignature, type InvoiceVerificationResult, InvoiceVerifier, type JsonRpcError, type JsonRpcRequest, type JsonRpcResponse, KEY_LENGTH, type KeyConfig, type KeyInfo, type L402Challenge, type L402Config, type L402Invoice, L402Middleware, type L402MiddlewareConfig, type L402Request, type L402Token, LiquidityAnalyzer, type LiquidityReport, type ListChannelsParams, type ListChannelsResult, type ListPeersResult, type MacaroonCaveat, MacaroonService, type MintParams, type Multiaddr, type NewInvoiceParams, type NewInvoiceResult, type NodeInfo, type NodeInfoResult, type OpenChannelParams, type OpenChannelResult, type OutPoint, type ParseInvoiceParams, type ParseInvoiceResult, type PaymentCustomRecords, type PaymentHash, type PaymentInfo, type PaymentStatus, type PeerId, type PeerInfo, type PolicyCheckResult, PolicyEngine, type PolicyViolation, type Privkey, type ProtectedResourceInfo, type Pubkey, type RateLimit, RateLimitSchema, type RecipientPolicy, RecipientPolicySchema, type RemoveTlcReason, type ResourceResolver, type ResourceResolverRegistry, type RevocationData, type RouterHop, SALT_LENGTH, SCRYPT_N, SCRYPT_P, SCRYPT_R, type Script$1 as Script, type SecurityPolicy, SecurityPolicySchema, type SendPaymentParams, type SendPaymentResult, type SendPaymentWithRouterParams, type SessionRoute, type SessionRouteNode, type SettleInvoiceParams, type SettlementData, type SettlementTlc, type ShutdownChannelParams, type SpendingLimit, SpendingLimitSchema, type TLCId, type TlcStatus, type UdtArgInfo, type UdtCellDep, type UdtCfgInfos, type UdtDep, type UdtScript, type UpdateChannelParams, type VerifyResult, type ViolationType, buildMultiaddr, buildMultiaddrFromNodeId, buildMultiaddrFromRpcUrl, ckbHash, ckbToShannons, collectBiscuitPermissions, createL402Middleware, decryptKey, derivePublicKey, fromHex, generatePreimage, generatePrivateKey, getBiscuitRuleForMethod, hashPreimage, isEncryptedKey, listSupportedBiscuitMethods, nodeIdToPeerId, randomBytes32, renderBiscuitFactsForMethods, renderBiscuitPermissionFacts, scriptToAddress, sha256Hash, shannonsToCkb, toHex, verifyPreimageHash };
|
package/dist/index.js
CHANGED
|
@@ -534,6 +534,166 @@ var LiquidityAnalyzer = class {
|
|
|
534
534
|
}
|
|
535
535
|
};
|
|
536
536
|
|
|
537
|
+
// src/l402/macaroon.ts
|
|
538
|
+
import { createHash } from "crypto";
|
|
539
|
+
import * as macaroon from "macaroon";
|
|
540
|
+
var MacaroonService = class {
|
|
541
|
+
rootKey;
|
|
542
|
+
constructor(rootKey) {
|
|
543
|
+
const keyHex = rootKey || process.env.L402_ROOT_KEY || this.generateRootKey();
|
|
544
|
+
this.rootKey = Buffer.from(keyHex.replace(/^0x/, ""), "hex");
|
|
545
|
+
if (this.rootKey.length !== 32) {
|
|
546
|
+
throw new Error("Root key must be 32 bytes (64 hex characters)");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Mint a new macaroon with embedded caveats.
|
|
551
|
+
*
|
|
552
|
+
* The macaroon identifier encodes the payment hash, resource info, and
|
|
553
|
+
* version. First-party caveats are added for payment_hash, expiry, and
|
|
554
|
+
* optionally resource_id / resource_type.
|
|
555
|
+
*/
|
|
556
|
+
mint(params) {
|
|
557
|
+
const expiryTimestamp = Math.floor(Date.now() / 1e3) + (params.expirySeconds || 3600);
|
|
558
|
+
const caveats = [
|
|
559
|
+
{ condition: "payment_hash", value: params.paymentHash },
|
|
560
|
+
{ condition: "expiry", value: expiryTimestamp.toString() }
|
|
561
|
+
];
|
|
562
|
+
if (params.resourceId) {
|
|
563
|
+
caveats.push({ condition: "resource_id", value: params.resourceId });
|
|
564
|
+
}
|
|
565
|
+
if (params.resourceType) {
|
|
566
|
+
caveats.push({ condition: "resource_type", value: params.resourceType });
|
|
567
|
+
}
|
|
568
|
+
const identifier = JSON.stringify({
|
|
569
|
+
v: 0,
|
|
570
|
+
pid: params.paymentHash,
|
|
571
|
+
rid: params.resourceId,
|
|
572
|
+
rtype: params.resourceType,
|
|
573
|
+
loc: params.location || "fiber-l402"
|
|
574
|
+
});
|
|
575
|
+
const m = macaroon.newMacaroon({
|
|
576
|
+
rootKey: this.rootKey,
|
|
577
|
+
identifier,
|
|
578
|
+
location: params.location || "fiber-l402"
|
|
579
|
+
});
|
|
580
|
+
for (const caveat of caveats) {
|
|
581
|
+
m.addFirstPartyCaveat(`${caveat.condition}=${caveat.value}`);
|
|
582
|
+
}
|
|
583
|
+
const exported = m.exportJSON();
|
|
584
|
+
const macaroonB64 = Buffer.from(JSON.stringify(exported)).toString("base64");
|
|
585
|
+
return { macaroon: macaroonB64, caveats };
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Verify a macaroon with a payment preimage.
|
|
589
|
+
*
|
|
590
|
+
* Validates:
|
|
591
|
+
* 1. SHA-256(preimage) === payment_hash in the macaroon identifier
|
|
592
|
+
* 2. Macaroon signature against root key
|
|
593
|
+
* 3. Expiry caveat is not past
|
|
594
|
+
*/
|
|
595
|
+
verify(macaroonB64, preimage) {
|
|
596
|
+
try {
|
|
597
|
+
const exported = JSON.parse(Buffer.from(macaroonB64, "base64").toString());
|
|
598
|
+
const m = macaroon.importMacaroon(exported);
|
|
599
|
+
const identifierBytes = m.identifier;
|
|
600
|
+
const identifier = Buffer.from(identifierBytes).toString("utf-8");
|
|
601
|
+
const idData = JSON.parse(identifier);
|
|
602
|
+
const paymentHash = idData.pid;
|
|
603
|
+
const preimageHash = createHash("sha256").update(Buffer.from(preimage.replace(/^0x/, ""), "hex")).digest("hex");
|
|
604
|
+
if (preimageHash !== paymentHash.replace(/^0x/, "")) {
|
|
605
|
+
return { valid: false, error: "Invalid preimage: hash mismatch" };
|
|
606
|
+
}
|
|
607
|
+
const caveatCheck = (caveat) => {
|
|
608
|
+
if (caveat.startsWith("expiry=")) {
|
|
609
|
+
const expiry = parseInt(caveat.split("=")[1], 10);
|
|
610
|
+
if (expiry < Math.floor(Date.now() / 1e3)) {
|
|
611
|
+
return "Macaroon expired";
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
};
|
|
616
|
+
m.verify(this.rootKey, caveatCheck, []);
|
|
617
|
+
const caveats = {};
|
|
618
|
+
for (const caveat of m.caveats) {
|
|
619
|
+
const caveatStr = Buffer.from(caveat.identifier).toString("utf-8");
|
|
620
|
+
const [key, value] = caveatStr.split("=");
|
|
621
|
+
if (key && value) {
|
|
622
|
+
caveats[key] = value;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return { valid: true, caveats };
|
|
626
|
+
} catch (error) {
|
|
627
|
+
return {
|
|
628
|
+
valid: false,
|
|
629
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Verify macaroon signature and caveats without requiring a preimage.
|
|
635
|
+
*
|
|
636
|
+
* Used in the "connected-node" flow where the middleware verifies
|
|
637
|
+
* invoice settlement via Fiber RPC instead of requiring the client
|
|
638
|
+
* to provide the preimage directly.
|
|
639
|
+
*/
|
|
640
|
+
verifyWithoutPreimage(macaroonB64) {
|
|
641
|
+
try {
|
|
642
|
+
const exported = JSON.parse(Buffer.from(macaroonB64, "base64").toString());
|
|
643
|
+
const m = macaroon.importMacaroon(exported);
|
|
644
|
+
const caveatCheck = (caveat) => {
|
|
645
|
+
if (caveat.startsWith("expiry=")) {
|
|
646
|
+
const expiry = parseInt(caveat.split("=")[1], 10);
|
|
647
|
+
if (expiry < Math.floor(Date.now() / 1e3)) {
|
|
648
|
+
return "Macaroon expired";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return null;
|
|
652
|
+
};
|
|
653
|
+
m.verify(this.rootKey, caveatCheck, []);
|
|
654
|
+
const caveats = {};
|
|
655
|
+
for (const caveat of m.caveats) {
|
|
656
|
+
const caveatStr = Buffer.from(caveat.identifier).toString("utf-8");
|
|
657
|
+
const [key, value] = caveatStr.split("=");
|
|
658
|
+
if (key && value) {
|
|
659
|
+
caveats[key] = value;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { valid: true, caveats };
|
|
663
|
+
} catch (error) {
|
|
664
|
+
return {
|
|
665
|
+
valid: false,
|
|
666
|
+
error: error instanceof Error ? error.message : "Verification failed"
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/** Extract caveats from a macaroon without full verification. */
|
|
671
|
+
extractCaveats(macaroonB64) {
|
|
672
|
+
try {
|
|
673
|
+
const exported = JSON.parse(Buffer.from(macaroonB64, "base64").toString());
|
|
674
|
+
const m = macaroon.importMacaroon(exported);
|
|
675
|
+
const caveats = {};
|
|
676
|
+
for (const caveat of m.caveats) {
|
|
677
|
+
const caveatStr = Buffer.from(caveat.identifier).toString("utf-8");
|
|
678
|
+
const parts = caveatStr.split("=");
|
|
679
|
+
if (parts.length === 2) {
|
|
680
|
+
caveats[parts[0]] = parts[1];
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return caveats;
|
|
684
|
+
} catch {
|
|
685
|
+
return {};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
generateRootKey() {
|
|
689
|
+
const bytes = new Uint8Array(32);
|
|
690
|
+
for (let i = 0; i < 32; i++) {
|
|
691
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
692
|
+
}
|
|
693
|
+
return Buffer.from(bytes).toString("hex");
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
537
697
|
// src/rpc/client.ts
|
|
538
698
|
var HASH_ALGORITHM_MAP = {
|
|
539
699
|
CkbHash: "ckb_hash",
|
|
@@ -991,6 +1151,237 @@ var FiberRpcClient = class {
|
|
|
991
1151
|
}
|
|
992
1152
|
};
|
|
993
1153
|
|
|
1154
|
+
// src/l402/middleware.ts
|
|
1155
|
+
var L402Middleware = class {
|
|
1156
|
+
config;
|
|
1157
|
+
rateLimitStore;
|
|
1158
|
+
macaroonService;
|
|
1159
|
+
rpcClient;
|
|
1160
|
+
resourceResolver;
|
|
1161
|
+
currency;
|
|
1162
|
+
constructor(config = {}) {
|
|
1163
|
+
this.config = {
|
|
1164
|
+
rootKey: config.rootKey || process.env.L402_ROOT_KEY,
|
|
1165
|
+
expirySeconds: config.expirySeconds || 3600,
|
|
1166
|
+
priceCkb: config.priceCkb || 0.1,
|
|
1167
|
+
rateLimitWindowMs: config.rateLimitWindowMs || 6e4,
|
|
1168
|
+
rateLimitMaxRequests: config.rateLimitMaxRequests || 100
|
|
1169
|
+
};
|
|
1170
|
+
this.rateLimitStore = /* @__PURE__ */ new Map();
|
|
1171
|
+
this.macaroonService = new MacaroonService(this.config.rootKey);
|
|
1172
|
+
this.currency = config.currency || "Fibt";
|
|
1173
|
+
this.rpcClient = config.rpcClient || new FiberRpcClient({
|
|
1174
|
+
url: config.rpcUrl || process.env.FIBER_RPC_URL || "http://127.0.0.1:8227",
|
|
1175
|
+
biscuitToken: config.biscuitToken || process.env.FIBER_BISCUIT_TOKEN
|
|
1176
|
+
});
|
|
1177
|
+
if (config.resourceResolver) {
|
|
1178
|
+
this.resourceResolver = config.resourceResolver;
|
|
1179
|
+
} else if (config.resourceProvider) {
|
|
1180
|
+
this.resourceResolver = {
|
|
1181
|
+
resolve: async (req) => config.resourceProvider?.(req)
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async handle(req, res, next) {
|
|
1186
|
+
const clientIp = req.ip || req.socket.remoteAddress || "unknown";
|
|
1187
|
+
if (!this.checkRateLimit(clientIp)) {
|
|
1188
|
+
res.status(429).json({
|
|
1189
|
+
error: "Rate limit exceeded",
|
|
1190
|
+
retryAfter: Math.ceil(this.config.rateLimitWindowMs / 1e3)
|
|
1191
|
+
});
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
const resource = this.resourceResolver ? await this.resourceResolver.resolve(req) : void 0;
|
|
1195
|
+
const authHeader = req.headers.authorization;
|
|
1196
|
+
if (!authHeader?.startsWith("L402 ")) {
|
|
1197
|
+
return this.issueChallenge(req, res, 402, void 0, resource);
|
|
1198
|
+
}
|
|
1199
|
+
const [, token] = authHeader.split(" ");
|
|
1200
|
+
if (!token) {
|
|
1201
|
+
return this.issueChallenge(req, res);
|
|
1202
|
+
}
|
|
1203
|
+
const splitIndex = token.indexOf(":");
|
|
1204
|
+
const macaroonToken = splitIndex >= 0 ? token.slice(0, splitIndex) : token;
|
|
1205
|
+
const preimage = splitIndex >= 0 ? token.slice(splitIndex + 1) : void 0;
|
|
1206
|
+
if (!macaroonToken) {
|
|
1207
|
+
return this.issueChallenge(req, res);
|
|
1208
|
+
}
|
|
1209
|
+
if (preimage) {
|
|
1210
|
+
const result = this.macaroonService.verify(macaroonToken, preimage);
|
|
1211
|
+
if (!result.valid) {
|
|
1212
|
+
return this.issueChallenge(req, res, 401, result.error, resource);
|
|
1213
|
+
}
|
|
1214
|
+
const headerError2 = this.validatePaymentHashHeader(req, result.caveats || {});
|
|
1215
|
+
if (headerError2) {
|
|
1216
|
+
return this.issueChallenge(req, res, 401, headerError2, resource);
|
|
1217
|
+
}
|
|
1218
|
+
const validateError2 = this.validateResourceCaveats(result.caveats || {}, resource);
|
|
1219
|
+
if (validateError2) {
|
|
1220
|
+
return this.issueChallenge(req, res, 401, validateError2, resource);
|
|
1221
|
+
}
|
|
1222
|
+
req.l402 = { valid: true, preimage };
|
|
1223
|
+
next();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const verifyResult = this.macaroonService.verifyWithoutPreimage(macaroonToken);
|
|
1227
|
+
if (!verifyResult.valid) {
|
|
1228
|
+
return this.issueChallenge(req, res, 401, verifyResult.error, resource);
|
|
1229
|
+
}
|
|
1230
|
+
const caveats = verifyResult.caveats || {};
|
|
1231
|
+
const paymentHash = caveats.payment_hash;
|
|
1232
|
+
if (!paymentHash) {
|
|
1233
|
+
return this.issueChallenge(req, res, 401, "Missing payment hash caveat", resource);
|
|
1234
|
+
}
|
|
1235
|
+
const headerError = this.validatePaymentHashHeader(req, caveats);
|
|
1236
|
+
if (headerError) {
|
|
1237
|
+
return this.issueChallenge(req, res, 401, headerError, resource);
|
|
1238
|
+
}
|
|
1239
|
+
const validateError = this.validateResourceCaveats(caveats, resource);
|
|
1240
|
+
if (validateError) {
|
|
1241
|
+
return this.issueChallenge(req, res, 401, validateError, resource);
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
const invoiceResult = await this.rpcClient.getInvoice({
|
|
1245
|
+
payment_hash: paymentHash
|
|
1246
|
+
});
|
|
1247
|
+
if (invoiceResult.status !== "Paid") {
|
|
1248
|
+
return this.issueChallenge(
|
|
1249
|
+
req,
|
|
1250
|
+
res,
|
|
1251
|
+
401,
|
|
1252
|
+
`Invoice not settled (status: ${invoiceResult.status})`,
|
|
1253
|
+
resource
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
} catch {
|
|
1257
|
+
return this.issueChallenge(req, res, 401, "Failed to verify invoice status", resource);
|
|
1258
|
+
}
|
|
1259
|
+
req.l402 = { valid: true, paymentHash };
|
|
1260
|
+
next();
|
|
1261
|
+
}
|
|
1262
|
+
// ─── Private Helpers ───────────────────────────────────────
|
|
1263
|
+
validateResourceCaveats(caveats, resource) {
|
|
1264
|
+
if (!resource) {
|
|
1265
|
+
return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
if (resource.id && caveats.resource_id && caveats.resource_id !== resource.id) {
|
|
1268
|
+
return "Resource id mismatch";
|
|
1269
|
+
}
|
|
1270
|
+
if (resource.type && caveats.resource_type && caveats.resource_type !== resource.type) {
|
|
1271
|
+
return "Resource type mismatch";
|
|
1272
|
+
}
|
|
1273
|
+
if (resource.id && !caveats.resource_id) {
|
|
1274
|
+
return "Missing resource_id in macaroon";
|
|
1275
|
+
}
|
|
1276
|
+
if (resource.type && !caveats.resource_type) {
|
|
1277
|
+
return "Missing resource_type in macaroon";
|
|
1278
|
+
}
|
|
1279
|
+
return void 0;
|
|
1280
|
+
}
|
|
1281
|
+
validatePaymentHashHeader(req, caveats) {
|
|
1282
|
+
const rawHeader = req.headers["x-l402-payment-hash"];
|
|
1283
|
+
const headerPaymentHash = Array.isArray(rawHeader) ? rawHeader[0] : rawHeader;
|
|
1284
|
+
if (!headerPaymentHash) {
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
const caveatPaymentHash = caveats.payment_hash;
|
|
1288
|
+
if (!caveatPaymentHash) {
|
|
1289
|
+
return "Missing payment hash caveat";
|
|
1290
|
+
}
|
|
1291
|
+
if (headerPaymentHash !== caveatPaymentHash) {
|
|
1292
|
+
return "Payment hash header mismatch";
|
|
1293
|
+
}
|
|
1294
|
+
return void 0;
|
|
1295
|
+
}
|
|
1296
|
+
checkRateLimit(ip) {
|
|
1297
|
+
const now = Date.now();
|
|
1298
|
+
const record = this.rateLimitStore.get(ip);
|
|
1299
|
+
if (!record) {
|
|
1300
|
+
this.rateLimitStore.set(ip, {
|
|
1301
|
+
count: 1,
|
|
1302
|
+
resetTime: now + this.config.rateLimitWindowMs
|
|
1303
|
+
});
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
if (now > record.resetTime) {
|
|
1307
|
+
this.rateLimitStore.set(ip, {
|
|
1308
|
+
count: 1,
|
|
1309
|
+
resetTime: now + this.config.rateLimitWindowMs
|
|
1310
|
+
});
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
if (record.count >= this.config.rateLimitMaxRequests) {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1316
|
+
record.count++;
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
async issueChallenge(req, res, statusCode = 402, errorMessage, resource) {
|
|
1320
|
+
try {
|
|
1321
|
+
const { macaroon: macaroon2, invoice } = await this.createChallenge(req, resource);
|
|
1322
|
+
const wwwAuthenticate = `L402 macaroon="${macaroon2}", invoice="${invoice}"`;
|
|
1323
|
+
res.status(statusCode).set("WWW-Authenticate", wwwAuthenticate).json({
|
|
1324
|
+
error: errorMessage || "Payment Required",
|
|
1325
|
+
macaroon: macaroon2,
|
|
1326
|
+
invoice
|
|
1327
|
+
});
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
res.status(500).json({
|
|
1330
|
+
error: "Failed to create payment challenge",
|
|
1331
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async createChallenge(req, resource) {
|
|
1336
|
+
const priceCkb = resource?.priceCkb ?? this.config.priceCkb;
|
|
1337
|
+
const amountShannons = `0x${(priceCkb * 1e8).toString(16)}`;
|
|
1338
|
+
const invoiceResult = await this.rpcClient.newInvoice({
|
|
1339
|
+
amount: amountShannons,
|
|
1340
|
+
description: `L402 Payment ${resource?.type ?? "resource"}`,
|
|
1341
|
+
currency: this.currency,
|
|
1342
|
+
expiry: `0x${this.config.expirySeconds.toString(16)}`,
|
|
1343
|
+
hash_algorithm: "Sha256"
|
|
1344
|
+
});
|
|
1345
|
+
const paymentHash = invoiceResult.invoice.data.payment_hash;
|
|
1346
|
+
const { macaroon: macaroon2 } = this.macaroonService.mint({
|
|
1347
|
+
identifier: `l402-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
1348
|
+
paymentHash,
|
|
1349
|
+
expirySeconds: this.config.expirySeconds,
|
|
1350
|
+
resourceId: resource?.id,
|
|
1351
|
+
resourceType: resource?.type,
|
|
1352
|
+
location: req.headers.host || "fiber-l402"
|
|
1353
|
+
});
|
|
1354
|
+
return { macaroon: macaroon2, invoice: invoiceResult.invoice_address };
|
|
1355
|
+
}
|
|
1356
|
+
};
|
|
1357
|
+
function createL402Middleware(config = {}) {
|
|
1358
|
+
const middleware = new L402Middleware(config);
|
|
1359
|
+
return middleware.handle.bind(middleware);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/l402/resources.ts
|
|
1363
|
+
var DefaultResourceResolverRegistry = class {
|
|
1364
|
+
resolvers;
|
|
1365
|
+
constructor(resolvers = []) {
|
|
1366
|
+
this.resolvers = [...resolvers];
|
|
1367
|
+
}
|
|
1368
|
+
register(resolver) {
|
|
1369
|
+
this.resolvers.push(resolver);
|
|
1370
|
+
}
|
|
1371
|
+
async resolve(req) {
|
|
1372
|
+
for (const resolver of this.resolvers) {
|
|
1373
|
+
if (!resolver.matches(req)) {
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
const resource = await resolver.resolve(req);
|
|
1377
|
+
if (resource) {
|
|
1378
|
+
return resource;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return void 0;
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
|
|
994
1385
|
// src/security/biscuit-policy.ts
|
|
995
1386
|
var RULES = {
|
|
996
1387
|
// Cch
|
|
@@ -1863,13 +2254,16 @@ var InvoiceVerifier = class {
|
|
|
1863
2254
|
export {
|
|
1864
2255
|
AUTH_TAG_LENGTH,
|
|
1865
2256
|
ChannelState,
|
|
2257
|
+
DefaultResourceResolverRegistry,
|
|
1866
2258
|
ENCRYPTED_MAGIC,
|
|
1867
2259
|
FiberRpcClient,
|
|
1868
2260
|
FiberRpcError,
|
|
1869
2261
|
IV_LENGTH,
|
|
1870
2262
|
InvoiceVerifier,
|
|
1871
2263
|
KEY_LENGTH,
|
|
2264
|
+
L402Middleware,
|
|
1872
2265
|
LiquidityAnalyzer,
|
|
2266
|
+
MacaroonService,
|
|
1873
2267
|
PolicyEngine,
|
|
1874
2268
|
SALT_LENGTH,
|
|
1875
2269
|
SCRYPT_N,
|
|
@@ -1881,6 +2275,7 @@ export {
|
|
|
1881
2275
|
ckbHash,
|
|
1882
2276
|
ckbToShannons,
|
|
1883
2277
|
collectBiscuitPermissions,
|
|
2278
|
+
createL402Middleware,
|
|
1884
2279
|
decryptKey,
|
|
1885
2280
|
derivePublicKey,
|
|
1886
2281
|
fromHex,
|