@mixrpay/agent-sdk 0.1.0 → 0.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.cjs CHANGED
@@ -204,6 +204,13 @@ var SessionKey = class _SessionKey {
204
204
  this.address = this.account.address;
205
205
  this.isTest = isTest;
206
206
  }
207
+ /**
208
+ * Get the private key as hex string (without 0x prefix).
209
+ * Used for API authentication headers.
210
+ */
211
+ get privateKeyHex() {
212
+ return this.privateKey.slice(2);
213
+ }
207
214
  /**
208
215
  * Parse a session key string into a SessionKey object.
209
216
  *
@@ -244,6 +251,18 @@ var SessionKey = class _SessionKey {
244
251
  message
245
252
  });
246
253
  }
254
+ /**
255
+ * Sign a plain message (EIP-191 personal sign).
256
+ *
257
+ * @param message - Message to sign
258
+ * @returns Hex-encoded signature
259
+ */
260
+ async signMessage(message) {
261
+ return (0, import_accounts.signMessage)({
262
+ privateKey: this.privateKey,
263
+ message
264
+ });
265
+ }
247
266
  /**
248
267
  * Get the chain ID based on whether this is a test key.
249
268
  */
@@ -276,6 +295,19 @@ function generateNonce() {
276
295
  crypto.getRandomValues(bytes);
277
296
  return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
278
297
  }
298
+ function buildSessionAuthMessage(timestamp, address) {
299
+ return `MixrPay:${timestamp}:${address.toLowerCase()}`;
300
+ }
301
+ async function createSessionAuthPayload(sessionKey) {
302
+ const timestamp = Date.now();
303
+ const message = buildSessionAuthMessage(timestamp, sessionKey.address);
304
+ const signature = await sessionKey.signMessage(message);
305
+ return {
306
+ address: sessionKey.address,
307
+ timestamp,
308
+ signature
309
+ };
310
+ }
279
311
 
280
312
  // src/x402.ts
281
313
  async function parse402Response(response) {
@@ -377,7 +409,7 @@ function validatePaymentAmount(amountUsd, maxPaymentUsd) {
377
409
 
378
410
  // src/agent-wallet.ts
379
411
  var SDK_VERSION = "0.1.0";
380
- var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "http://localhost:3000";
412
+ var DEFAULT_BASE_URL = process.env.MIXRPAY_BASE_URL || "https://mixrpay.com";
381
413
  var DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator";
382
414
  var DEFAULT_TIMEOUT = 3e4;
383
415
  var NETWORKS = {
@@ -953,6 +985,385 @@ var AgentWallet = class {
953
985
  setLogLevel(level) {
954
986
  this.logger.setLevel(level);
955
987
  }
988
+ // ===========================================================================
989
+ // Session Authorization Methods (for MixrPay Merchants)
990
+ // ===========================================================================
991
+ /**
992
+ * Create the X-Session-Auth header for secure API authentication.
993
+ * Uses signature-based authentication - private key is NEVER transmitted.
994
+ *
995
+ * @returns Headers object with X-Session-Auth
996
+ */
997
+ async getSessionAuthHeaders() {
998
+ const payload = await createSessionAuthPayload(this.sessionKey);
999
+ return {
1000
+ "X-Session-Auth": JSON.stringify(payload)
1001
+ };
1002
+ }
1003
+ /**
1004
+ * Get an existing session or create a new one with a MixrPay merchant.
1005
+ *
1006
+ * This is the recommended way to interact with MixrPay-enabled APIs.
1007
+ * If an active session exists, it will be returned. Otherwise, a new
1008
+ * session authorization request will be created and confirmed.
1009
+ *
1010
+ * @param options - Session creation options
1011
+ * @returns Active session authorization
1012
+ *
1013
+ * @throws {MixrPayError} If merchant is not found or session creation fails
1014
+ *
1015
+ * @example
1016
+ * ```typescript
1017
+ * const session = await wallet.getOrCreateSession({
1018
+ * merchantPublicKey: 'pk_live_abc123...',
1019
+ * spendingLimitUsd: 25.00,
1020
+ * durationDays: 7,
1021
+ * });
1022
+ *
1023
+ * console.log(`Session active: $${session.remainingLimitUsd} remaining`);
1024
+ * ```
1025
+ */
1026
+ async getOrCreateSession(options) {
1027
+ this.logger.debug("getOrCreateSession called", options);
1028
+ const { merchantPublicKey, spendingLimitUsd = 25, durationDays = 7 } = options;
1029
+ try {
1030
+ const existingSession = await this.getSessionByMerchant(merchantPublicKey);
1031
+ if (existingSession && existingSession.status === "active") {
1032
+ this.logger.debug("Found existing active session", existingSession.id);
1033
+ return existingSession;
1034
+ }
1035
+ } catch {
1036
+ }
1037
+ this.logger.info(`Creating new session with merchant ${merchantPublicKey.slice(0, 20)}...`);
1038
+ const authHeaders = await this.getSessionAuthHeaders();
1039
+ const authorizeResponse = await fetch(`${this.baseUrl}/api/v2/session/authorize`, {
1040
+ method: "POST",
1041
+ headers: {
1042
+ "Content-Type": "application/json",
1043
+ ...authHeaders
1044
+ },
1045
+ body: JSON.stringify({
1046
+ merchant_public_key: merchantPublicKey,
1047
+ spending_limit_usd: spendingLimitUsd,
1048
+ duration_days: durationDays
1049
+ })
1050
+ });
1051
+ if (!authorizeResponse.ok) {
1052
+ const error = await authorizeResponse.json().catch(() => ({}));
1053
+ throw new MixrPayError(
1054
+ error.message || error.error || `Failed to create session: ${authorizeResponse.status}`
1055
+ );
1056
+ }
1057
+ const authorizeData = await authorizeResponse.json();
1058
+ const sessionId = authorizeData.session_id;
1059
+ const messageToSign = authorizeData.message_to_sign;
1060
+ if (!sessionId || !messageToSign) {
1061
+ throw new MixrPayError("Invalid authorize response: missing session_id or message_to_sign");
1062
+ }
1063
+ this.logger.debug("Signing session authorization message...");
1064
+ const signature = await this.sessionKey.signMessage(messageToSign);
1065
+ const confirmResponse = await fetch(`${this.baseUrl}/api/v2/session/confirm`, {
1066
+ method: "POST",
1067
+ headers: {
1068
+ "Content-Type": "application/json"
1069
+ },
1070
+ body: JSON.stringify({
1071
+ session_id: sessionId,
1072
+ signature,
1073
+ wallet_address: this.walletAddress
1074
+ })
1075
+ });
1076
+ if (!confirmResponse.ok) {
1077
+ const error = await confirmResponse.json().catch(() => ({}));
1078
+ throw new MixrPayError(
1079
+ error.message || `Failed to confirm session: ${confirmResponse.status}`
1080
+ );
1081
+ }
1082
+ const confirmData = await confirmResponse.json();
1083
+ this.logger.info(`Session created: ${confirmData.session?.id || sessionId}`);
1084
+ return this.parseSessionResponse(confirmData.session || confirmData);
1085
+ }
1086
+ /**
1087
+ * Get session status for a specific merchant.
1088
+ *
1089
+ * @param merchantPublicKey - The merchant's public key
1090
+ * @returns Session authorization or null if not found
1091
+ */
1092
+ async getSessionByMerchant(merchantPublicKey) {
1093
+ this.logger.debug("getSessionByMerchant", merchantPublicKey);
1094
+ const authHeaders = await this.getSessionAuthHeaders();
1095
+ const response = await fetch(`${this.baseUrl}/api/v2/session/status?merchant_public_key=${encodeURIComponent(merchantPublicKey)}`, {
1096
+ headers: authHeaders
1097
+ });
1098
+ if (response.status === 404) {
1099
+ return null;
1100
+ }
1101
+ if (!response.ok) {
1102
+ const error = await response.json().catch(() => ({}));
1103
+ throw new MixrPayError(error.message || `Failed to get session: ${response.status}`);
1104
+ }
1105
+ const data = await response.json();
1106
+ return data.session ? this.parseSessionResponse(data.session) : null;
1107
+ }
1108
+ /**
1109
+ * List all session authorizations for this wallet.
1110
+ *
1111
+ * @returns Array of session authorizations
1112
+ *
1113
+ * @example
1114
+ * ```typescript
1115
+ * const sessions = await wallet.listSessions();
1116
+ * for (const session of sessions) {
1117
+ * console.log(`${session.merchantName}: $${session.remainingLimitUsd} remaining`);
1118
+ * }
1119
+ * ```
1120
+ */
1121
+ async listSessions() {
1122
+ this.logger.debug("listSessions");
1123
+ const authHeaders = await this.getSessionAuthHeaders();
1124
+ const response = await fetch(`${this.baseUrl}/api/v2/session/list`, {
1125
+ headers: authHeaders
1126
+ });
1127
+ if (!response.ok) {
1128
+ const error = await response.json().catch(() => ({}));
1129
+ throw new MixrPayError(error.message || `Failed to list sessions: ${response.status}`);
1130
+ }
1131
+ const data = await response.json();
1132
+ return (data.sessions || []).map((s) => this.parseSessionResponse(s));
1133
+ }
1134
+ /**
1135
+ * Revoke a session authorization.
1136
+ *
1137
+ * After revocation, no further charges can be made against this session.
1138
+ *
1139
+ * @param sessionId - The session ID to revoke
1140
+ * @returns true if revoked successfully
1141
+ *
1142
+ * @example
1143
+ * ```typescript
1144
+ * const revoked = await wallet.revokeSession('sess_abc123');
1145
+ * if (revoked) {
1146
+ * console.log('Session revoked successfully');
1147
+ * }
1148
+ * ```
1149
+ */
1150
+ async revokeSession(sessionId) {
1151
+ this.logger.debug("revokeSession", sessionId);
1152
+ const authHeaders = await this.getSessionAuthHeaders();
1153
+ const response = await fetch(`${this.baseUrl}/api/v2/session/revoke`, {
1154
+ method: "POST",
1155
+ headers: {
1156
+ "Content-Type": "application/json",
1157
+ ...authHeaders
1158
+ },
1159
+ body: JSON.stringify({ session_id: sessionId })
1160
+ });
1161
+ if (!response.ok) {
1162
+ const error = await response.json().catch(() => ({}));
1163
+ this.logger.error("Failed to revoke session:", error);
1164
+ return false;
1165
+ }
1166
+ this.logger.info(`Session ${sessionId} revoked`);
1167
+ return true;
1168
+ }
1169
+ /**
1170
+ * Charge against an active session authorization.
1171
+ *
1172
+ * This is useful when you need to manually charge a session outside of
1173
+ * the `callMerchantApi()` flow.
1174
+ *
1175
+ * @param sessionId - The session ID to charge
1176
+ * @param amountUsd - Amount to charge in USD
1177
+ * @param options - Additional charge options
1178
+ * @returns Charge result
1179
+ *
1180
+ * @example
1181
+ * ```typescript
1182
+ * const result = await wallet.chargeSession('sess_abc123', 0.05, {
1183
+ * feature: 'ai-generation',
1184
+ * idempotencyKey: 'unique-key-123',
1185
+ * });
1186
+ *
1187
+ * console.log(`Charged $${result.amountUsd}, remaining: $${result.remainingSessionBalanceUsd}`);
1188
+ * ```
1189
+ */
1190
+ async chargeSession(sessionId, amountUsd, options = {}) {
1191
+ this.logger.debug("chargeSession", { sessionId, amountUsd, options });
1192
+ const authHeaders = await this.getSessionAuthHeaders();
1193
+ const response = await fetch(`${this.baseUrl}/api/v2/charge`, {
1194
+ method: "POST",
1195
+ headers: {
1196
+ "Content-Type": "application/json",
1197
+ ...authHeaders
1198
+ },
1199
+ body: JSON.stringify({
1200
+ session_id: sessionId,
1201
+ price_usd: amountUsd,
1202
+ feature: options.feature,
1203
+ idempotency_key: options.idempotencyKey,
1204
+ metadata: options.metadata
1205
+ })
1206
+ });
1207
+ if (!response.ok) {
1208
+ const error = await response.json().catch(() => ({}));
1209
+ if (response.status === 402) {
1210
+ if (error.error === "session_limit_exceeded") {
1211
+ throw new SpendingLimitExceededError(
1212
+ "session",
1213
+ error.sessionLimitUsd || error.session_limit_usd || 0,
1214
+ amountUsd
1215
+ );
1216
+ }
1217
+ if (error.error === "insufficient_balance") {
1218
+ throw new InsufficientBalanceError(
1219
+ amountUsd,
1220
+ error.availableUsd || error.available_usd || 0
1221
+ );
1222
+ }
1223
+ }
1224
+ throw new MixrPayError(error.message || `Charge failed: ${response.status}`);
1225
+ }
1226
+ const data = await response.json();
1227
+ this.logger.payment(amountUsd, sessionId, options.feature);
1228
+ return {
1229
+ success: true,
1230
+ chargeId: data.chargeId || data.charge_id,
1231
+ amountUsd: data.amountUsd || data.amount_usd || amountUsd,
1232
+ txHash: data.txHash || data.tx_hash,
1233
+ remainingSessionBalanceUsd: data.remainingSessionBalanceUsd || data.remaining_session_balance_usd || 0
1234
+ };
1235
+ }
1236
+ /**
1237
+ * Call a MixrPay merchant's API with automatic session management.
1238
+ *
1239
+ * This is the recommended way to interact with MixrPay-enabled APIs.
1240
+ * It automatically:
1241
+ * 1. Gets or creates a session authorization
1242
+ * 2. Adds the `X-Mixr-Session` header to the request
1243
+ * 3. Handles payment errors and session expiration
1244
+ *
1245
+ * @param options - API call options
1246
+ * @returns Response from the merchant API
1247
+ *
1248
+ * @example
1249
+ * ```typescript
1250
+ * const response = await wallet.callMerchantApi({
1251
+ * url: 'https://api.merchant.com/generate',
1252
+ * merchantPublicKey: 'pk_live_abc123...',
1253
+ * method: 'POST',
1254
+ * body: { prompt: 'Hello world' },
1255
+ * priceUsd: 0.05,
1256
+ * });
1257
+ *
1258
+ * const data = await response.json();
1259
+ * ```
1260
+ */
1261
+ async callMerchantApi(options) {
1262
+ const {
1263
+ url,
1264
+ method = "POST",
1265
+ body,
1266
+ headers: customHeaders = {},
1267
+ merchantPublicKey,
1268
+ priceUsd,
1269
+ feature
1270
+ } = options;
1271
+ this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
1272
+ if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
1273
+ throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
1274
+ }
1275
+ const session = await this.getOrCreateSession({
1276
+ merchantPublicKey,
1277
+ spendingLimitUsd: 25,
1278
+ // Default limit
1279
+ durationDays: 7
1280
+ });
1281
+ const headers = {
1282
+ "Content-Type": "application/json",
1283
+ "X-Mixr-Session": session.id,
1284
+ ...customHeaders
1285
+ };
1286
+ if (feature) {
1287
+ headers["X-Mixr-Feature"] = feature;
1288
+ }
1289
+ const requestBody = body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0;
1290
+ const response = await fetch(url, {
1291
+ method,
1292
+ headers,
1293
+ body: requestBody,
1294
+ signal: AbortSignal.timeout(this.timeout)
1295
+ });
1296
+ const chargedAmount = response.headers.get("X-Mixr-Charged");
1297
+ if (chargedAmount) {
1298
+ const amountUsd = parseFloat(chargedAmount);
1299
+ if (!isNaN(amountUsd)) {
1300
+ const payment = {
1301
+ amountUsd,
1302
+ recipient: merchantPublicKey,
1303
+ txHash: response.headers.get("X-Payment-TxHash"),
1304
+ timestamp: /* @__PURE__ */ new Date(),
1305
+ description: feature || "API call",
1306
+ url
1307
+ };
1308
+ this.payments.push(payment);
1309
+ this.totalSpentUsd += amountUsd;
1310
+ this.logger.payment(amountUsd, merchantPublicKey, feature);
1311
+ if (this.onPayment) {
1312
+ this.onPayment(payment);
1313
+ }
1314
+ }
1315
+ }
1316
+ if (response.status === 402) {
1317
+ const errorData = await response.json().catch(() => ({}));
1318
+ if (errorData.error === "session_expired") {
1319
+ this.logger.info("Session expired, creating new one...");
1320
+ const newSession = await this.getOrCreateSession({
1321
+ merchantPublicKey,
1322
+ spendingLimitUsd: 25,
1323
+ durationDays: 7
1324
+ });
1325
+ headers["X-Mixr-Session"] = newSession.id;
1326
+ return fetch(url, {
1327
+ method,
1328
+ headers,
1329
+ body: requestBody,
1330
+ signal: AbortSignal.timeout(this.timeout)
1331
+ });
1332
+ }
1333
+ if (errorData.error === "session_limit_exceeded") {
1334
+ throw new SpendingLimitExceededError(
1335
+ "session",
1336
+ errorData.sessionLimitUsd || 0,
1337
+ priceUsd || 0
1338
+ );
1339
+ }
1340
+ if (errorData.error === "insufficient_balance") {
1341
+ throw new InsufficientBalanceError(
1342
+ priceUsd || 0,
1343
+ errorData.availableUsd || 0
1344
+ );
1345
+ }
1346
+ }
1347
+ return response;
1348
+ }
1349
+ /**
1350
+ * Parse session response data into SessionAuthorization object.
1351
+ */
1352
+ parseSessionResponse(data) {
1353
+ return {
1354
+ id: data.id || data.session_id || data.sessionId,
1355
+ merchantId: data.merchantId || data.merchant_id,
1356
+ merchantName: data.merchantName || data.merchant_name || "Unknown",
1357
+ status: data.status || "active",
1358
+ spendingLimitUsd: Number(data.spendingLimitUsd || data.spending_limit_usd || data.spendingLimit || 0),
1359
+ amountUsedUsd: Number(data.amountUsedUsd || data.amount_used_usd || data.amountUsed || 0),
1360
+ remainingLimitUsd: Number(
1361
+ data.remainingLimitUsd || data.remaining_limit_usd || data.remainingLimit || Number(data.spendingLimitUsd || data.spending_limit_usd || 0) - Number(data.amountUsedUsd || data.amount_used_usd || 0)
1362
+ ),
1363
+ expiresAt: new Date(data.expiresAt || data.expires_at),
1364
+ createdAt: new Date(data.createdAt || data.created_at)
1365
+ };
1366
+ }
956
1367
  };
957
1368
  // Annotate the CommonJS export names for ESM import in node:
958
1369
  0 && (module.exports = {