@mixrpay/agent-sdk 0.1.1 → 0.3.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/README.md +208 -221
- package/dist/index.cjs +555 -51
- package/dist/index.d.cts +356 -239
- package/dist/index.d.ts +356 -239
- package/dist/index.js +552 -37
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -21,28 +21,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
AgentWallet: () => AgentWallet,
|
|
24
|
-
DEFAULT_BASE_URL: () => DEFAULT_BASE_URL,
|
|
25
|
-
DEFAULT_FACILITATOR_URL: () => DEFAULT_FACILITATOR_URL,
|
|
26
|
-
DEFAULT_TIMEOUT: () => DEFAULT_TIMEOUT,
|
|
27
24
|
InsufficientBalanceError: () => InsufficientBalanceError,
|
|
28
25
|
InvalidSessionKeyError: () => InvalidSessionKeyError,
|
|
29
26
|
MixrPayError: () => MixrPayError,
|
|
30
|
-
NETWORKS: () => NETWORKS,
|
|
31
27
|
PaymentFailedError: () => PaymentFailedError,
|
|
32
28
|
SDK_VERSION: () => SDK_VERSION,
|
|
33
|
-
|
|
29
|
+
SessionExpiredError: () => SessionExpiredError,
|
|
34
30
|
SessionKeyExpiredError: () => SessionKeyExpiredError,
|
|
31
|
+
SessionLimitExceededError: () => SessionLimitExceededError,
|
|
32
|
+
SessionNotFoundError: () => SessionNotFoundError,
|
|
33
|
+
SessionRevokedError: () => SessionRevokedError,
|
|
35
34
|
SpendingLimitExceededError: () => SpendingLimitExceededError,
|
|
36
|
-
|
|
37
|
-
buildTransferAuthorizationData: () => buildTransferAuthorizationData,
|
|
38
|
-
buildXPaymentHeader: () => buildXPaymentHeader,
|
|
39
|
-
generateNonce: () => generateNonce,
|
|
40
|
-
getAmountUsd: () => getAmountUsd,
|
|
41
|
-
getErrorMessage: () => getErrorMessage,
|
|
42
|
-
isMixrPayError: () => isMixrPayError,
|
|
43
|
-
isPaymentExpired: () => isPaymentExpired,
|
|
44
|
-
parse402Response: () => parse402Response,
|
|
45
|
-
validatePaymentAmount: () => validatePaymentAmount
|
|
35
|
+
isMixrPayError: () => isMixrPayError
|
|
46
36
|
});
|
|
47
37
|
module.exports = __toCommonJS(index_exports);
|
|
48
38
|
|
|
@@ -159,17 +149,71 @@ var X402ProtocolError = class extends MixrPayError {
|
|
|
159
149
|
this.reason = reason;
|
|
160
150
|
}
|
|
161
151
|
};
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
152
|
+
var SessionExpiredError = class extends MixrPayError {
|
|
153
|
+
/** ID of the expired session */
|
|
154
|
+
sessionId;
|
|
155
|
+
/** When the session expired (ISO string or Date) */
|
|
156
|
+
expiredAt;
|
|
157
|
+
constructor(sessionId, expiredAt) {
|
|
158
|
+
super(
|
|
159
|
+
`Session ${sessionId} has expired${expiredAt ? ` at ${expiredAt}` : ""}. A new session will be created automatically on your next request.`,
|
|
160
|
+
"SESSION_EXPIRED"
|
|
161
|
+
);
|
|
162
|
+
this.name = "SessionExpiredError";
|
|
163
|
+
this.sessionId = sessionId;
|
|
164
|
+
this.expiredAt = expiredAt;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var SessionLimitExceededError = class extends MixrPayError {
|
|
168
|
+
/** ID of the session */
|
|
169
|
+
sessionId;
|
|
170
|
+
/** The session's spending limit in USD */
|
|
171
|
+
limit;
|
|
172
|
+
/** The amount requested in USD */
|
|
173
|
+
requested;
|
|
174
|
+
/** Remaining balance in the session (limit - used) */
|
|
175
|
+
remaining;
|
|
176
|
+
constructor(limit, requested, remaining, sessionId) {
|
|
177
|
+
super(
|
|
178
|
+
`Session spending limit exceeded: limit is $${limit.toFixed(2)}, requested $${requested.toFixed(2)}, remaining $${remaining.toFixed(2)}. Create a new session with a higher limit to continue.`,
|
|
179
|
+
"SESSION_LIMIT_EXCEEDED"
|
|
180
|
+
);
|
|
181
|
+
this.name = "SessionLimitExceededError";
|
|
182
|
+
this.sessionId = sessionId;
|
|
183
|
+
this.limit = limit;
|
|
184
|
+
this.requested = requested;
|
|
185
|
+
this.remaining = remaining;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var SessionNotFoundError = class extends MixrPayError {
|
|
189
|
+
/** The session ID that was not found */
|
|
190
|
+
sessionId;
|
|
191
|
+
constructor(sessionId) {
|
|
192
|
+
super(
|
|
193
|
+
`Session ${sessionId} not found. It may have been deleted or never existed. Create a new session with getOrCreateSession().`,
|
|
194
|
+
"SESSION_NOT_FOUND"
|
|
195
|
+
);
|
|
196
|
+
this.name = "SessionNotFoundError";
|
|
197
|
+
this.sessionId = sessionId;
|
|
168
198
|
}
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
};
|
|
200
|
+
var SessionRevokedError = class extends MixrPayError {
|
|
201
|
+
/** The session ID that was revoked */
|
|
202
|
+
sessionId;
|
|
203
|
+
/** Optional reason for revocation */
|
|
204
|
+
reason;
|
|
205
|
+
constructor(sessionId, reason) {
|
|
206
|
+
super(
|
|
207
|
+
`Session ${sessionId} has been revoked${reason ? `: ${reason}` : ""}. Create a new session with getOrCreateSession().`,
|
|
208
|
+
"SESSION_REVOKED"
|
|
209
|
+
);
|
|
210
|
+
this.name = "SessionRevokedError";
|
|
211
|
+
this.sessionId = sessionId;
|
|
212
|
+
this.reason = reason;
|
|
171
213
|
}
|
|
172
|
-
|
|
214
|
+
};
|
|
215
|
+
function isMixrPayError(error) {
|
|
216
|
+
return error instanceof MixrPayError;
|
|
173
217
|
}
|
|
174
218
|
|
|
175
219
|
// src/session-key.ts
|
|
@@ -204,6 +248,13 @@ var SessionKey = class _SessionKey {
|
|
|
204
248
|
this.address = this.account.address;
|
|
205
249
|
this.isTest = isTest;
|
|
206
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Get the private key as hex string (without 0x prefix).
|
|
253
|
+
* Used for API authentication headers.
|
|
254
|
+
*/
|
|
255
|
+
get privateKeyHex() {
|
|
256
|
+
return this.privateKey.slice(2);
|
|
257
|
+
}
|
|
207
258
|
/**
|
|
208
259
|
* Parse a session key string into a SessionKey object.
|
|
209
260
|
*
|
|
@@ -244,6 +295,18 @@ var SessionKey = class _SessionKey {
|
|
|
244
295
|
message
|
|
245
296
|
});
|
|
246
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Sign a plain message (EIP-191 personal sign).
|
|
300
|
+
*
|
|
301
|
+
* @param message - Message to sign
|
|
302
|
+
* @returns Hex-encoded signature
|
|
303
|
+
*/
|
|
304
|
+
async signMessage(message) {
|
|
305
|
+
return (0, import_accounts.signMessage)({
|
|
306
|
+
privateKey: this.privateKey,
|
|
307
|
+
message
|
|
308
|
+
});
|
|
309
|
+
}
|
|
247
310
|
/**
|
|
248
311
|
* Get the chain ID based on whether this is a test key.
|
|
249
312
|
*/
|
|
@@ -276,6 +339,19 @@ function generateNonce() {
|
|
|
276
339
|
crypto.getRandomValues(bytes);
|
|
277
340
|
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
278
341
|
}
|
|
342
|
+
function buildSessionAuthMessage(timestamp, address) {
|
|
343
|
+
return `MixrPay:${timestamp}:${address.toLowerCase()}`;
|
|
344
|
+
}
|
|
345
|
+
async function createSessionAuthPayload(sessionKey) {
|
|
346
|
+
const timestamp = Date.now();
|
|
347
|
+
const message = buildSessionAuthMessage(timestamp, sessionKey.address);
|
|
348
|
+
const signature = await sessionKey.signMessage(message);
|
|
349
|
+
return {
|
|
350
|
+
address: sessionKey.address,
|
|
351
|
+
timestamp,
|
|
352
|
+
signature
|
|
353
|
+
};
|
|
354
|
+
}
|
|
279
355
|
|
|
280
356
|
// src/x402.ts
|
|
281
357
|
async function parse402Response(response) {
|
|
@@ -364,21 +440,10 @@ function isPaymentExpired(requirements) {
|
|
|
364
440
|
function getAmountUsd(requirements) {
|
|
365
441
|
return Number(requirements.amount) / 1e6;
|
|
366
442
|
}
|
|
367
|
-
function validatePaymentAmount(amountUsd, maxPaymentUsd) {
|
|
368
|
-
if (amountUsd <= 0) {
|
|
369
|
-
throw new X402ProtocolError(`Invalid payment amount: $${amountUsd.toFixed(2)}`);
|
|
370
|
-
}
|
|
371
|
-
if (maxPaymentUsd !== void 0 && amountUsd > maxPaymentUsd) {
|
|
372
|
-
throw new X402ProtocolError(
|
|
373
|
-
`Payment amount $${amountUsd.toFixed(2)} exceeds client limit $${maxPaymentUsd.toFixed(2)}`
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
443
|
|
|
378
444
|
// src/agent-wallet.ts
|
|
379
|
-
var SDK_VERSION = "0.
|
|
445
|
+
var SDK_VERSION = "0.3.0";
|
|
380
446
|
var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://mixrpay.com";
|
|
381
|
-
var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
|
|
382
447
|
var DEFAULT_TIMEOUT = 3e4;
|
|
383
448
|
var NETWORKS = {
|
|
384
449
|
BASE_MAINNET: { chainId: 8453, name: "Base", isTestnet: false },
|
|
@@ -953,30 +1018,469 @@ var AgentWallet = class {
|
|
|
953
1018
|
setLogLevel(level) {
|
|
954
1019
|
this.logger.setLevel(level);
|
|
955
1020
|
}
|
|
1021
|
+
// ===========================================================================
|
|
1022
|
+
// Session Authorization Methods (for MixrPay Merchants)
|
|
1023
|
+
// ===========================================================================
|
|
1024
|
+
/**
|
|
1025
|
+
* Create the X-Session-Auth header for secure API authentication.
|
|
1026
|
+
* Uses signature-based authentication - private key is NEVER transmitted.
|
|
1027
|
+
*
|
|
1028
|
+
* @returns Headers object with X-Session-Auth
|
|
1029
|
+
*/
|
|
1030
|
+
async getSessionAuthHeaders() {
|
|
1031
|
+
const payload = await createSessionAuthPayload(this.sessionKey);
|
|
1032
|
+
return {
|
|
1033
|
+
"X-Session-Auth": JSON.stringify(payload)
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Get an existing session or create a new one with a MixrPay merchant.
|
|
1038
|
+
*
|
|
1039
|
+
* This is the recommended way to interact with MixrPay-enabled APIs.
|
|
1040
|
+
* If an active session exists, it will be returned. Otherwise, a new
|
|
1041
|
+
* session authorization request will be created and confirmed.
|
|
1042
|
+
*
|
|
1043
|
+
* @param options - Session creation options
|
|
1044
|
+
* @returns Active session authorization
|
|
1045
|
+
*
|
|
1046
|
+
* @throws {MixrPayError} If merchant is not found or session creation fails
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```typescript
|
|
1050
|
+
* const session = await wallet.getOrCreateSession({
|
|
1051
|
+
* merchantPublicKey: 'pk_live_abc123...',
|
|
1052
|
+
* spendingLimitUsd: 25.00,
|
|
1053
|
+
* durationDays: 7,
|
|
1054
|
+
* });
|
|
1055
|
+
*
|
|
1056
|
+
* console.log(`Session active: $${session.remainingLimitUsd} remaining`);
|
|
1057
|
+
* ```
|
|
1058
|
+
*/
|
|
1059
|
+
async getOrCreateSession(options) {
|
|
1060
|
+
this.logger.debug("getOrCreateSession called", options);
|
|
1061
|
+
const { merchantPublicKey, spendingLimitUsd = 25, durationDays = 7 } = options;
|
|
1062
|
+
try {
|
|
1063
|
+
const existingSession = await this.getSessionByMerchant(merchantPublicKey);
|
|
1064
|
+
if (existingSession && existingSession.status === "active") {
|
|
1065
|
+
this.logger.debug("Found existing active session", existingSession.id);
|
|
1066
|
+
return existingSession;
|
|
1067
|
+
}
|
|
1068
|
+
} catch {
|
|
1069
|
+
}
|
|
1070
|
+
this.logger.info(`Creating new session with merchant ${merchantPublicKey.slice(0, 20)}...`);
|
|
1071
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1072
|
+
const authorizeResponse = await fetch(`${this.baseUrl}/api/v2/session/authorize`, {
|
|
1073
|
+
method: "POST",
|
|
1074
|
+
headers: {
|
|
1075
|
+
"Content-Type": "application/json",
|
|
1076
|
+
...authHeaders
|
|
1077
|
+
},
|
|
1078
|
+
body: JSON.stringify({
|
|
1079
|
+
merchant_public_key: merchantPublicKey,
|
|
1080
|
+
spending_limit_usd: spendingLimitUsd,
|
|
1081
|
+
duration_days: durationDays
|
|
1082
|
+
})
|
|
1083
|
+
});
|
|
1084
|
+
if (!authorizeResponse.ok) {
|
|
1085
|
+
const error = await authorizeResponse.json().catch(() => ({}));
|
|
1086
|
+
throw new MixrPayError(
|
|
1087
|
+
error.message || error.error || `Failed to create session: ${authorizeResponse.status}`
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
const authorizeData = await authorizeResponse.json();
|
|
1091
|
+
const sessionId = authorizeData.session_id;
|
|
1092
|
+
const messageToSign = authorizeData.message_to_sign;
|
|
1093
|
+
if (!sessionId || !messageToSign) {
|
|
1094
|
+
throw new MixrPayError("Invalid authorize response: missing session_id or message_to_sign");
|
|
1095
|
+
}
|
|
1096
|
+
this.logger.debug("Signing session authorization message...");
|
|
1097
|
+
const signature = await this.sessionKey.signMessage(messageToSign);
|
|
1098
|
+
const confirmResponse = await fetch(`${this.baseUrl}/api/v2/session/confirm`, {
|
|
1099
|
+
method: "POST",
|
|
1100
|
+
headers: {
|
|
1101
|
+
"Content-Type": "application/json"
|
|
1102
|
+
},
|
|
1103
|
+
body: JSON.stringify({
|
|
1104
|
+
session_id: sessionId,
|
|
1105
|
+
signature,
|
|
1106
|
+
wallet_address: this.walletAddress
|
|
1107
|
+
})
|
|
1108
|
+
});
|
|
1109
|
+
if (!confirmResponse.ok) {
|
|
1110
|
+
const error = await confirmResponse.json().catch(() => ({}));
|
|
1111
|
+
throw new MixrPayError(
|
|
1112
|
+
error.message || `Failed to confirm session: ${confirmResponse.status}`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
const confirmData = await confirmResponse.json();
|
|
1116
|
+
this.logger.info(`Session created: ${confirmData.session?.id || sessionId}`);
|
|
1117
|
+
return this.parseSessionResponse(confirmData.session || confirmData);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Get session status for a specific merchant.
|
|
1121
|
+
*
|
|
1122
|
+
* @param merchantPublicKey - The merchant's public key
|
|
1123
|
+
* @returns Session authorization or null if not found
|
|
1124
|
+
*/
|
|
1125
|
+
async getSessionByMerchant(merchantPublicKey) {
|
|
1126
|
+
this.logger.debug("getSessionByMerchant", merchantPublicKey);
|
|
1127
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1128
|
+
const response = await fetch(`${this.baseUrl}/api/v2/session/status?merchant_public_key=${encodeURIComponent(merchantPublicKey)}`, {
|
|
1129
|
+
headers: authHeaders
|
|
1130
|
+
});
|
|
1131
|
+
if (response.status === 404) {
|
|
1132
|
+
return null;
|
|
1133
|
+
}
|
|
1134
|
+
if (!response.ok) {
|
|
1135
|
+
const error = await response.json().catch(() => ({}));
|
|
1136
|
+
throw new MixrPayError(error.message || `Failed to get session: ${response.status}`);
|
|
1137
|
+
}
|
|
1138
|
+
const data = await response.json();
|
|
1139
|
+
return data.session ? this.parseSessionResponse(data.session) : null;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* List all session authorizations for this wallet.
|
|
1143
|
+
*
|
|
1144
|
+
* @returns Array of session authorizations
|
|
1145
|
+
*
|
|
1146
|
+
* @example
|
|
1147
|
+
* ```typescript
|
|
1148
|
+
* const sessions = await wallet.listSessions();
|
|
1149
|
+
* for (const session of sessions) {
|
|
1150
|
+
* console.log(`${session.merchantName}: $${session.remainingLimitUsd} remaining`);
|
|
1151
|
+
* }
|
|
1152
|
+
* ```
|
|
1153
|
+
*/
|
|
1154
|
+
async listSessions() {
|
|
1155
|
+
this.logger.debug("listSessions");
|
|
1156
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1157
|
+
const response = await fetch(`${this.baseUrl}/api/v2/session/list`, {
|
|
1158
|
+
headers: authHeaders
|
|
1159
|
+
});
|
|
1160
|
+
if (!response.ok) {
|
|
1161
|
+
const error = await response.json().catch(() => ({}));
|
|
1162
|
+
throw new MixrPayError(error.message || `Failed to list sessions: ${response.status}`);
|
|
1163
|
+
}
|
|
1164
|
+
const data = await response.json();
|
|
1165
|
+
return (data.sessions || []).map((s) => this.parseSessionResponse(s));
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Revoke a session authorization.
|
|
1169
|
+
*
|
|
1170
|
+
* After revocation, no further charges can be made against this session.
|
|
1171
|
+
*
|
|
1172
|
+
* @param sessionId - The session ID to revoke
|
|
1173
|
+
* @returns true if revoked successfully
|
|
1174
|
+
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* ```typescript
|
|
1177
|
+
* const revoked = await wallet.revokeSession('sess_abc123');
|
|
1178
|
+
* if (revoked) {
|
|
1179
|
+
* console.log('Session revoked successfully');
|
|
1180
|
+
* }
|
|
1181
|
+
* ```
|
|
1182
|
+
*/
|
|
1183
|
+
async revokeSession(sessionId) {
|
|
1184
|
+
this.logger.debug("revokeSession", sessionId);
|
|
1185
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1186
|
+
const response = await fetch(`${this.baseUrl}/api/v2/session/revoke`, {
|
|
1187
|
+
method: "POST",
|
|
1188
|
+
headers: {
|
|
1189
|
+
"Content-Type": "application/json",
|
|
1190
|
+
...authHeaders
|
|
1191
|
+
},
|
|
1192
|
+
body: JSON.stringify({ session_id: sessionId })
|
|
1193
|
+
});
|
|
1194
|
+
if (!response.ok) {
|
|
1195
|
+
const error = await response.json().catch(() => ({}));
|
|
1196
|
+
this.logger.error("Failed to revoke session:", error);
|
|
1197
|
+
return false;
|
|
1198
|
+
}
|
|
1199
|
+
this.logger.info(`Session ${sessionId} revoked`);
|
|
1200
|
+
return true;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Get statistics about all session authorizations.
|
|
1204
|
+
*
|
|
1205
|
+
* This provides an overview of active, expired, and revoked sessions,
|
|
1206
|
+
* along with aggregate spending information.
|
|
1207
|
+
*
|
|
1208
|
+
* @returns Session statistics
|
|
1209
|
+
*
|
|
1210
|
+
* @example
|
|
1211
|
+
* ```typescript
|
|
1212
|
+
* const stats = await wallet.getSessionStats();
|
|
1213
|
+
* console.log(`Active sessions: ${stats.activeCount}`);
|
|
1214
|
+
* console.log(`Total authorized: $${stats.totalAuthorizedUsd.toFixed(2)}`);
|
|
1215
|
+
* console.log(`Total remaining: $${stats.totalRemainingUsd.toFixed(2)}`);
|
|
1216
|
+
*
|
|
1217
|
+
* for (const session of stats.activeSessions) {
|
|
1218
|
+
* console.log(`${session.merchantName}: $${session.remainingUsd} remaining`);
|
|
1219
|
+
* }
|
|
1220
|
+
* ```
|
|
1221
|
+
*/
|
|
1222
|
+
async getSessionStats() {
|
|
1223
|
+
this.logger.debug("getSessionStats");
|
|
1224
|
+
const sessions = await this.listSessions();
|
|
1225
|
+
const now = /* @__PURE__ */ new Date();
|
|
1226
|
+
const active = [];
|
|
1227
|
+
const expired = [];
|
|
1228
|
+
const revoked = [];
|
|
1229
|
+
for (const session of sessions) {
|
|
1230
|
+
if (session.status === "revoked") {
|
|
1231
|
+
revoked.push(session);
|
|
1232
|
+
} else if (session.status === "expired" || session.expiresAt && session.expiresAt < now) {
|
|
1233
|
+
expired.push(session);
|
|
1234
|
+
} else if (session.status === "active") {
|
|
1235
|
+
active.push(session);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
const totalAuthorizedUsd = active.reduce((sum, s) => sum + s.spendingLimitUsd, 0);
|
|
1239
|
+
const totalSpentUsd = sessions.reduce((sum, s) => sum + s.amountUsedUsd, 0);
|
|
1240
|
+
const totalRemainingUsd = active.reduce((sum, s) => sum + s.remainingLimitUsd, 0);
|
|
1241
|
+
return {
|
|
1242
|
+
activeCount: active.length,
|
|
1243
|
+
expiredCount: expired.length,
|
|
1244
|
+
revokedCount: revoked.length,
|
|
1245
|
+
totalAuthorizedUsd,
|
|
1246
|
+
totalSpentUsd,
|
|
1247
|
+
totalRemainingUsd,
|
|
1248
|
+
activeSessions: active.map((s) => ({
|
|
1249
|
+
id: s.id,
|
|
1250
|
+
merchantName: s.merchantName,
|
|
1251
|
+
merchantPublicKey: s.merchantId,
|
|
1252
|
+
// merchantId is the public key in this context
|
|
1253
|
+
spendingLimitUsd: s.spendingLimitUsd,
|
|
1254
|
+
remainingUsd: s.remainingLimitUsd,
|
|
1255
|
+
expiresAt: s.expiresAt
|
|
1256
|
+
}))
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Charge against an active session authorization.
|
|
1261
|
+
*
|
|
1262
|
+
* This is useful when you need to manually charge a session outside of
|
|
1263
|
+
* the `callMerchantApi()` flow.
|
|
1264
|
+
*
|
|
1265
|
+
* @param sessionId - The session ID to charge
|
|
1266
|
+
* @param amountUsd - Amount to charge in USD
|
|
1267
|
+
* @param options - Additional charge options
|
|
1268
|
+
* @returns Charge result
|
|
1269
|
+
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```typescript
|
|
1272
|
+
* const result = await wallet.chargeSession('sess_abc123', 0.05, {
|
|
1273
|
+
* feature: 'ai-generation',
|
|
1274
|
+
* idempotencyKey: 'unique-key-123',
|
|
1275
|
+
* });
|
|
1276
|
+
*
|
|
1277
|
+
* console.log(`Charged $${result.amountUsd}, remaining: $${result.remainingSessionBalanceUsd}`);
|
|
1278
|
+
* ```
|
|
1279
|
+
*/
|
|
1280
|
+
async chargeSession(sessionId, amountUsd, options = {}) {
|
|
1281
|
+
this.logger.debug("chargeSession", { sessionId, amountUsd, options });
|
|
1282
|
+
const authHeaders = await this.getSessionAuthHeaders();
|
|
1283
|
+
const response = await fetch(`${this.baseUrl}/api/v2/charge`, {
|
|
1284
|
+
method: "POST",
|
|
1285
|
+
headers: {
|
|
1286
|
+
"Content-Type": "application/json",
|
|
1287
|
+
...authHeaders
|
|
1288
|
+
},
|
|
1289
|
+
body: JSON.stringify({
|
|
1290
|
+
session_id: sessionId,
|
|
1291
|
+
price_usd: amountUsd,
|
|
1292
|
+
feature: options.feature,
|
|
1293
|
+
idempotency_key: options.idempotencyKey,
|
|
1294
|
+
metadata: options.metadata
|
|
1295
|
+
})
|
|
1296
|
+
});
|
|
1297
|
+
if (!response.ok) {
|
|
1298
|
+
const error = await response.json().catch(() => ({}));
|
|
1299
|
+
const errorCode = error.error || error.error_code || "";
|
|
1300
|
+
if (response.status === 402) {
|
|
1301
|
+
if (errorCode === "session_limit_exceeded") {
|
|
1302
|
+
const limit = error.sessionLimitUsd || error.session_limit_usd || 0;
|
|
1303
|
+
const remaining = error.remainingUsd || error.remaining_usd || 0;
|
|
1304
|
+
throw new SessionLimitExceededError(limit, amountUsd, remaining, sessionId);
|
|
1305
|
+
}
|
|
1306
|
+
if (errorCode === "insufficient_balance") {
|
|
1307
|
+
throw new InsufficientBalanceError(
|
|
1308
|
+
amountUsd,
|
|
1309
|
+
error.availableUsd || error.available_usd || 0
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (response.status === 404 || errorCode === "session_not_found") {
|
|
1314
|
+
throw new SessionNotFoundError(sessionId);
|
|
1315
|
+
}
|
|
1316
|
+
if (errorCode === "session_expired") {
|
|
1317
|
+
throw new SessionExpiredError(sessionId, error.expiredAt || error.expires_at);
|
|
1318
|
+
}
|
|
1319
|
+
if (errorCode === "session_revoked") {
|
|
1320
|
+
throw new SessionRevokedError(sessionId, error.reason);
|
|
1321
|
+
}
|
|
1322
|
+
throw new MixrPayError(error.message || `Charge failed: ${response.status}`);
|
|
1323
|
+
}
|
|
1324
|
+
const data = await response.json();
|
|
1325
|
+
this.logger.payment(amountUsd, sessionId, options.feature);
|
|
1326
|
+
return {
|
|
1327
|
+
success: true,
|
|
1328
|
+
chargeId: data.chargeId || data.charge_id,
|
|
1329
|
+
amountUsd: data.amountUsd || data.amount_usd || amountUsd,
|
|
1330
|
+
txHash: data.txHash || data.tx_hash,
|
|
1331
|
+
remainingSessionBalanceUsd: data.remainingSessionBalanceUsd || data.remaining_session_balance_usd || 0
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Call a MixrPay merchant's API with automatic session management.
|
|
1336
|
+
*
|
|
1337
|
+
* This is the recommended way to interact with MixrPay-enabled APIs.
|
|
1338
|
+
* It automatically:
|
|
1339
|
+
* 1. Gets or creates a session authorization
|
|
1340
|
+
* 2. Adds the `X-Mixr-Session` header to the request
|
|
1341
|
+
* 3. Handles payment errors and session expiration
|
|
1342
|
+
*
|
|
1343
|
+
* @param options - API call options
|
|
1344
|
+
* @returns Response from the merchant API
|
|
1345
|
+
*
|
|
1346
|
+
* @example
|
|
1347
|
+
* ```typescript
|
|
1348
|
+
* const response = await wallet.callMerchantApi({
|
|
1349
|
+
* url: 'https://api.merchant.com/generate',
|
|
1350
|
+
* merchantPublicKey: 'pk_live_abc123...',
|
|
1351
|
+
* method: 'POST',
|
|
1352
|
+
* body: { prompt: 'Hello world' },
|
|
1353
|
+
* priceUsd: 0.05,
|
|
1354
|
+
* });
|
|
1355
|
+
*
|
|
1356
|
+
* const data = await response.json();
|
|
1357
|
+
* ```
|
|
1358
|
+
*/
|
|
1359
|
+
async callMerchantApi(options) {
|
|
1360
|
+
const {
|
|
1361
|
+
url,
|
|
1362
|
+
method = "POST",
|
|
1363
|
+
body,
|
|
1364
|
+
headers: customHeaders = {},
|
|
1365
|
+
merchantPublicKey,
|
|
1366
|
+
priceUsd,
|
|
1367
|
+
feature
|
|
1368
|
+
} = options;
|
|
1369
|
+
this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
|
|
1370
|
+
if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
|
|
1371
|
+
throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
|
|
1372
|
+
}
|
|
1373
|
+
const session = await this.getOrCreateSession({
|
|
1374
|
+
merchantPublicKey,
|
|
1375
|
+
spendingLimitUsd: 25,
|
|
1376
|
+
// Default limit
|
|
1377
|
+
durationDays: 7
|
|
1378
|
+
});
|
|
1379
|
+
const headers = {
|
|
1380
|
+
"Content-Type": "application/json",
|
|
1381
|
+
"X-Mixr-Session": session.id,
|
|
1382
|
+
...customHeaders
|
|
1383
|
+
};
|
|
1384
|
+
if (feature) {
|
|
1385
|
+
headers["X-Mixr-Feature"] = feature;
|
|
1386
|
+
}
|
|
1387
|
+
const requestBody = body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0;
|
|
1388
|
+
const response = await fetch(url, {
|
|
1389
|
+
method,
|
|
1390
|
+
headers,
|
|
1391
|
+
body: requestBody,
|
|
1392
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1393
|
+
});
|
|
1394
|
+
const chargedAmount = response.headers.get("X-Mixr-Charged");
|
|
1395
|
+
if (chargedAmount) {
|
|
1396
|
+
const amountUsd = parseFloat(chargedAmount);
|
|
1397
|
+
if (!isNaN(amountUsd)) {
|
|
1398
|
+
const payment = {
|
|
1399
|
+
amountUsd,
|
|
1400
|
+
recipient: merchantPublicKey,
|
|
1401
|
+
txHash: response.headers.get("X-Payment-TxHash"),
|
|
1402
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1403
|
+
description: feature || "API call",
|
|
1404
|
+
url
|
|
1405
|
+
};
|
|
1406
|
+
this.payments.push(payment);
|
|
1407
|
+
this.totalSpentUsd += amountUsd;
|
|
1408
|
+
this.logger.payment(amountUsd, merchantPublicKey, feature);
|
|
1409
|
+
if (this.onPayment) {
|
|
1410
|
+
this.onPayment(payment);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (response.status === 402) {
|
|
1415
|
+
const errorData = await response.json().catch(() => ({}));
|
|
1416
|
+
const errorCode = errorData.error || errorData.error_code || "";
|
|
1417
|
+
if (errorCode === "session_expired") {
|
|
1418
|
+
this.logger.info("Session expired, creating new one...");
|
|
1419
|
+
const newSession = await this.getOrCreateSession({
|
|
1420
|
+
merchantPublicKey,
|
|
1421
|
+
spendingLimitUsd: 25,
|
|
1422
|
+
durationDays: 7
|
|
1423
|
+
});
|
|
1424
|
+
headers["X-Mixr-Session"] = newSession.id;
|
|
1425
|
+
return fetch(url, {
|
|
1426
|
+
method,
|
|
1427
|
+
headers,
|
|
1428
|
+
body: requestBody,
|
|
1429
|
+
signal: AbortSignal.timeout(this.timeout)
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
if (errorCode === "session_limit_exceeded") {
|
|
1433
|
+
const limit = errorData.sessionLimitUsd || errorData.session_limit_usd || 0;
|
|
1434
|
+
const remaining = errorData.remainingUsd || errorData.remaining_usd || 0;
|
|
1435
|
+
throw new SessionLimitExceededError(limit, priceUsd || 0, remaining, session.id);
|
|
1436
|
+
}
|
|
1437
|
+
if (errorCode === "session_revoked") {
|
|
1438
|
+
throw new SessionRevokedError(session.id, errorData.reason);
|
|
1439
|
+
}
|
|
1440
|
+
if (errorCode === "session_not_found") {
|
|
1441
|
+
throw new SessionNotFoundError(session.id);
|
|
1442
|
+
}
|
|
1443
|
+
if (errorCode === "insufficient_balance") {
|
|
1444
|
+
throw new InsufficientBalanceError(
|
|
1445
|
+
priceUsd || 0,
|
|
1446
|
+
errorData.availableUsd || errorData.available_usd || 0
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return response;
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1453
|
+
* Parse session response data into SessionAuthorization object.
|
|
1454
|
+
*/
|
|
1455
|
+
parseSessionResponse(data) {
|
|
1456
|
+
return {
|
|
1457
|
+
id: data.id || data.session_id || data.sessionId,
|
|
1458
|
+
merchantId: data.merchantId || data.merchant_id,
|
|
1459
|
+
merchantName: data.merchantName || data.merchant_name || "Unknown",
|
|
1460
|
+
status: data.status || "active",
|
|
1461
|
+
spendingLimitUsd: Number(data.spendingLimitUsd || data.spending_limit_usd || data.spendingLimit || 0),
|
|
1462
|
+
amountUsedUsd: Number(data.amountUsedUsd || data.amount_used_usd || data.amountUsed || 0),
|
|
1463
|
+
remainingLimitUsd: Number(
|
|
1464
|
+
data.remainingLimitUsd || data.remaining_limit_usd || data.remainingLimit || Number(data.spendingLimitUsd || data.spending_limit_usd || 0) - Number(data.amountUsedUsd || data.amount_used_usd || 0)
|
|
1465
|
+
),
|
|
1466
|
+
expiresAt: new Date(data.expiresAt || data.expires_at),
|
|
1467
|
+
createdAt: new Date(data.createdAt || data.created_at)
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
956
1470
|
};
|
|
957
1471
|
// Annotate the CommonJS export names for ESM import in node:
|
|
958
1472
|
0 && (module.exports = {
|
|
959
1473
|
AgentWallet,
|
|
960
|
-
DEFAULT_BASE_URL,
|
|
961
|
-
DEFAULT_FACILITATOR_URL,
|
|
962
|
-
DEFAULT_TIMEOUT,
|
|
963
1474
|
InsufficientBalanceError,
|
|
964
1475
|
InvalidSessionKeyError,
|
|
965
1476
|
MixrPayError,
|
|
966
|
-
NETWORKS,
|
|
967
1477
|
PaymentFailedError,
|
|
968
1478
|
SDK_VERSION,
|
|
969
|
-
|
|
1479
|
+
SessionExpiredError,
|
|
970
1480
|
SessionKeyExpiredError,
|
|
1481
|
+
SessionLimitExceededError,
|
|
1482
|
+
SessionNotFoundError,
|
|
1483
|
+
SessionRevokedError,
|
|
971
1484
|
SpendingLimitExceededError,
|
|
972
|
-
|
|
973
|
-
buildTransferAuthorizationData,
|
|
974
|
-
buildXPaymentHeader,
|
|
975
|
-
generateNonce,
|
|
976
|
-
getAmountUsd,
|
|
977
|
-
getErrorMessage,
|
|
978
|
-
isMixrPayError,
|
|
979
|
-
isPaymentExpired,
|
|
980
|
-
parse402Response,
|
|
981
|
-
validatePaymentAmount
|
|
1485
|
+
isMixrPayError
|
|
982
1486
|
});
|