@mixrpay/agent-sdk 0.1.1 → 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.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/session-key.ts
2
2
  import {
3
3
  privateKeyToAccount,
4
- signTypedData
4
+ signTypedData,
5
+ signMessage as viemSignMessage
5
6
  } from "viem/accounts";
6
7
 
7
8
  // src/errors.ts
@@ -159,6 +160,13 @@ var SessionKey = class _SessionKey {
159
160
  this.address = this.account.address;
160
161
  this.isTest = isTest;
161
162
  }
163
+ /**
164
+ * Get the private key as hex string (without 0x prefix).
165
+ * Used for API authentication headers.
166
+ */
167
+ get privateKeyHex() {
168
+ return this.privateKey.slice(2);
169
+ }
162
170
  /**
163
171
  * Parse a session key string into a SessionKey object.
164
172
  *
@@ -199,6 +207,18 @@ var SessionKey = class _SessionKey {
199
207
  message
200
208
  });
201
209
  }
210
+ /**
211
+ * Sign a plain message (EIP-191 personal sign).
212
+ *
213
+ * @param message - Message to sign
214
+ * @returns Hex-encoded signature
215
+ */
216
+ async signMessage(message) {
217
+ return viemSignMessage({
218
+ privateKey: this.privateKey,
219
+ message
220
+ });
221
+ }
202
222
  /**
203
223
  * Get the chain ID based on whether this is a test key.
204
224
  */
@@ -231,6 +251,19 @@ function generateNonce() {
231
251
  crypto.getRandomValues(bytes);
232
252
  return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
233
253
  }
254
+ function buildSessionAuthMessage(timestamp, address) {
255
+ return `MixrPay:${timestamp}:${address.toLowerCase()}`;
256
+ }
257
+ async function createSessionAuthPayload(sessionKey) {
258
+ const timestamp = Date.now();
259
+ const message = buildSessionAuthMessage(timestamp, sessionKey.address);
260
+ const signature = await sessionKey.signMessage(message);
261
+ return {
262
+ address: sessionKey.address,
263
+ timestamp,
264
+ signature
265
+ };
266
+ }
234
267
 
235
268
  // src/x402.ts
236
269
  async function parse402Response(response) {
@@ -908,6 +941,385 @@ var AgentWallet = class {
908
941
  setLogLevel(level) {
909
942
  this.logger.setLevel(level);
910
943
  }
944
+ // ===========================================================================
945
+ // Session Authorization Methods (for MixrPay Merchants)
946
+ // ===========================================================================
947
+ /**
948
+ * Create the X-Session-Auth header for secure API authentication.
949
+ * Uses signature-based authentication - private key is NEVER transmitted.
950
+ *
951
+ * @returns Headers object with X-Session-Auth
952
+ */
953
+ async getSessionAuthHeaders() {
954
+ const payload = await createSessionAuthPayload(this.sessionKey);
955
+ return {
956
+ "X-Session-Auth": JSON.stringify(payload)
957
+ };
958
+ }
959
+ /**
960
+ * Get an existing session or create a new one with a MixrPay merchant.
961
+ *
962
+ * This is the recommended way to interact with MixrPay-enabled APIs.
963
+ * If an active session exists, it will be returned. Otherwise, a new
964
+ * session authorization request will be created and confirmed.
965
+ *
966
+ * @param options - Session creation options
967
+ * @returns Active session authorization
968
+ *
969
+ * @throws {MixrPayError} If merchant is not found or session creation fails
970
+ *
971
+ * @example
972
+ * ```typescript
973
+ * const session = await wallet.getOrCreateSession({
974
+ * merchantPublicKey: 'pk_live_abc123...',
975
+ * spendingLimitUsd: 25.00,
976
+ * durationDays: 7,
977
+ * });
978
+ *
979
+ * console.log(`Session active: $${session.remainingLimitUsd} remaining`);
980
+ * ```
981
+ */
982
+ async getOrCreateSession(options) {
983
+ this.logger.debug("getOrCreateSession called", options);
984
+ const { merchantPublicKey, spendingLimitUsd = 25, durationDays = 7 } = options;
985
+ try {
986
+ const existingSession = await this.getSessionByMerchant(merchantPublicKey);
987
+ if (existingSession && existingSession.status === "active") {
988
+ this.logger.debug("Found existing active session", existingSession.id);
989
+ return existingSession;
990
+ }
991
+ } catch {
992
+ }
993
+ this.logger.info(`Creating new session with merchant ${merchantPublicKey.slice(0, 20)}...`);
994
+ const authHeaders = await this.getSessionAuthHeaders();
995
+ const authorizeResponse = await fetch(`${this.baseUrl}/api/v2/session/authorize`, {
996
+ method: "POST",
997
+ headers: {
998
+ "Content-Type": "application/json",
999
+ ...authHeaders
1000
+ },
1001
+ body: JSON.stringify({
1002
+ merchant_public_key: merchantPublicKey,
1003
+ spending_limit_usd: spendingLimitUsd,
1004
+ duration_days: durationDays
1005
+ })
1006
+ });
1007
+ if (!authorizeResponse.ok) {
1008
+ const error = await authorizeResponse.json().catch(() => ({}));
1009
+ throw new MixrPayError(
1010
+ error.message || error.error || `Failed to create session: ${authorizeResponse.status}`
1011
+ );
1012
+ }
1013
+ const authorizeData = await authorizeResponse.json();
1014
+ const sessionId = authorizeData.session_id;
1015
+ const messageToSign = authorizeData.message_to_sign;
1016
+ if (!sessionId || !messageToSign) {
1017
+ throw new MixrPayError("Invalid authorize response: missing session_id or message_to_sign");
1018
+ }
1019
+ this.logger.debug("Signing session authorization message...");
1020
+ const signature = await this.sessionKey.signMessage(messageToSign);
1021
+ const confirmResponse = await fetch(`${this.baseUrl}/api/v2/session/confirm`, {
1022
+ method: "POST",
1023
+ headers: {
1024
+ "Content-Type": "application/json"
1025
+ },
1026
+ body: JSON.stringify({
1027
+ session_id: sessionId,
1028
+ signature,
1029
+ wallet_address: this.walletAddress
1030
+ })
1031
+ });
1032
+ if (!confirmResponse.ok) {
1033
+ const error = await confirmResponse.json().catch(() => ({}));
1034
+ throw new MixrPayError(
1035
+ error.message || `Failed to confirm session: ${confirmResponse.status}`
1036
+ );
1037
+ }
1038
+ const confirmData = await confirmResponse.json();
1039
+ this.logger.info(`Session created: ${confirmData.session?.id || sessionId}`);
1040
+ return this.parseSessionResponse(confirmData.session || confirmData);
1041
+ }
1042
+ /**
1043
+ * Get session status for a specific merchant.
1044
+ *
1045
+ * @param merchantPublicKey - The merchant's public key
1046
+ * @returns Session authorization or null if not found
1047
+ */
1048
+ async getSessionByMerchant(merchantPublicKey) {
1049
+ this.logger.debug("getSessionByMerchant", merchantPublicKey);
1050
+ const authHeaders = await this.getSessionAuthHeaders();
1051
+ const response = await fetch(`${this.baseUrl}/api/v2/session/status?merchant_public_key=${encodeURIComponent(merchantPublicKey)}`, {
1052
+ headers: authHeaders
1053
+ });
1054
+ if (response.status === 404) {
1055
+ return null;
1056
+ }
1057
+ if (!response.ok) {
1058
+ const error = await response.json().catch(() => ({}));
1059
+ throw new MixrPayError(error.message || `Failed to get session: ${response.status}`);
1060
+ }
1061
+ const data = await response.json();
1062
+ return data.session ? this.parseSessionResponse(data.session) : null;
1063
+ }
1064
+ /**
1065
+ * List all session authorizations for this wallet.
1066
+ *
1067
+ * @returns Array of session authorizations
1068
+ *
1069
+ * @example
1070
+ * ```typescript
1071
+ * const sessions = await wallet.listSessions();
1072
+ * for (const session of sessions) {
1073
+ * console.log(`${session.merchantName}: $${session.remainingLimitUsd} remaining`);
1074
+ * }
1075
+ * ```
1076
+ */
1077
+ async listSessions() {
1078
+ this.logger.debug("listSessions");
1079
+ const authHeaders = await this.getSessionAuthHeaders();
1080
+ const response = await fetch(`${this.baseUrl}/api/v2/session/list`, {
1081
+ headers: authHeaders
1082
+ });
1083
+ if (!response.ok) {
1084
+ const error = await response.json().catch(() => ({}));
1085
+ throw new MixrPayError(error.message || `Failed to list sessions: ${response.status}`);
1086
+ }
1087
+ const data = await response.json();
1088
+ return (data.sessions || []).map((s) => this.parseSessionResponse(s));
1089
+ }
1090
+ /**
1091
+ * Revoke a session authorization.
1092
+ *
1093
+ * After revocation, no further charges can be made against this session.
1094
+ *
1095
+ * @param sessionId - The session ID to revoke
1096
+ * @returns true if revoked successfully
1097
+ *
1098
+ * @example
1099
+ * ```typescript
1100
+ * const revoked = await wallet.revokeSession('sess_abc123');
1101
+ * if (revoked) {
1102
+ * console.log('Session revoked successfully');
1103
+ * }
1104
+ * ```
1105
+ */
1106
+ async revokeSession(sessionId) {
1107
+ this.logger.debug("revokeSession", sessionId);
1108
+ const authHeaders = await this.getSessionAuthHeaders();
1109
+ const response = await fetch(`${this.baseUrl}/api/v2/session/revoke`, {
1110
+ method: "POST",
1111
+ headers: {
1112
+ "Content-Type": "application/json",
1113
+ ...authHeaders
1114
+ },
1115
+ body: JSON.stringify({ session_id: sessionId })
1116
+ });
1117
+ if (!response.ok) {
1118
+ const error = await response.json().catch(() => ({}));
1119
+ this.logger.error("Failed to revoke session:", error);
1120
+ return false;
1121
+ }
1122
+ this.logger.info(`Session ${sessionId} revoked`);
1123
+ return true;
1124
+ }
1125
+ /**
1126
+ * Charge against an active session authorization.
1127
+ *
1128
+ * This is useful when you need to manually charge a session outside of
1129
+ * the `callMerchantApi()` flow.
1130
+ *
1131
+ * @param sessionId - The session ID to charge
1132
+ * @param amountUsd - Amount to charge in USD
1133
+ * @param options - Additional charge options
1134
+ * @returns Charge result
1135
+ *
1136
+ * @example
1137
+ * ```typescript
1138
+ * const result = await wallet.chargeSession('sess_abc123', 0.05, {
1139
+ * feature: 'ai-generation',
1140
+ * idempotencyKey: 'unique-key-123',
1141
+ * });
1142
+ *
1143
+ * console.log(`Charged $${result.amountUsd}, remaining: $${result.remainingSessionBalanceUsd}`);
1144
+ * ```
1145
+ */
1146
+ async chargeSession(sessionId, amountUsd, options = {}) {
1147
+ this.logger.debug("chargeSession", { sessionId, amountUsd, options });
1148
+ const authHeaders = await this.getSessionAuthHeaders();
1149
+ const response = await fetch(`${this.baseUrl}/api/v2/charge`, {
1150
+ method: "POST",
1151
+ headers: {
1152
+ "Content-Type": "application/json",
1153
+ ...authHeaders
1154
+ },
1155
+ body: JSON.stringify({
1156
+ session_id: sessionId,
1157
+ price_usd: amountUsd,
1158
+ feature: options.feature,
1159
+ idempotency_key: options.idempotencyKey,
1160
+ metadata: options.metadata
1161
+ })
1162
+ });
1163
+ if (!response.ok) {
1164
+ const error = await response.json().catch(() => ({}));
1165
+ if (response.status === 402) {
1166
+ if (error.error === "session_limit_exceeded") {
1167
+ throw new SpendingLimitExceededError(
1168
+ "session",
1169
+ error.sessionLimitUsd || error.session_limit_usd || 0,
1170
+ amountUsd
1171
+ );
1172
+ }
1173
+ if (error.error === "insufficient_balance") {
1174
+ throw new InsufficientBalanceError(
1175
+ amountUsd,
1176
+ error.availableUsd || error.available_usd || 0
1177
+ );
1178
+ }
1179
+ }
1180
+ throw new MixrPayError(error.message || `Charge failed: ${response.status}`);
1181
+ }
1182
+ const data = await response.json();
1183
+ this.logger.payment(amountUsd, sessionId, options.feature);
1184
+ return {
1185
+ success: true,
1186
+ chargeId: data.chargeId || data.charge_id,
1187
+ amountUsd: data.amountUsd || data.amount_usd || amountUsd,
1188
+ txHash: data.txHash || data.tx_hash,
1189
+ remainingSessionBalanceUsd: data.remainingSessionBalanceUsd || data.remaining_session_balance_usd || 0
1190
+ };
1191
+ }
1192
+ /**
1193
+ * Call a MixrPay merchant's API with automatic session management.
1194
+ *
1195
+ * This is the recommended way to interact with MixrPay-enabled APIs.
1196
+ * It automatically:
1197
+ * 1. Gets or creates a session authorization
1198
+ * 2. Adds the `X-Mixr-Session` header to the request
1199
+ * 3. Handles payment errors and session expiration
1200
+ *
1201
+ * @param options - API call options
1202
+ * @returns Response from the merchant API
1203
+ *
1204
+ * @example
1205
+ * ```typescript
1206
+ * const response = await wallet.callMerchantApi({
1207
+ * url: 'https://api.merchant.com/generate',
1208
+ * merchantPublicKey: 'pk_live_abc123...',
1209
+ * method: 'POST',
1210
+ * body: { prompt: 'Hello world' },
1211
+ * priceUsd: 0.05,
1212
+ * });
1213
+ *
1214
+ * const data = await response.json();
1215
+ * ```
1216
+ */
1217
+ async callMerchantApi(options) {
1218
+ const {
1219
+ url,
1220
+ method = "POST",
1221
+ body,
1222
+ headers: customHeaders = {},
1223
+ merchantPublicKey,
1224
+ priceUsd,
1225
+ feature
1226
+ } = options;
1227
+ this.logger.debug("callMerchantApi", { url, method, merchantPublicKey, priceUsd });
1228
+ if (priceUsd !== void 0 && this.maxPaymentUsd !== void 0 && priceUsd > this.maxPaymentUsd) {
1229
+ throw new SpendingLimitExceededError("client_max", this.maxPaymentUsd, priceUsd);
1230
+ }
1231
+ const session = await this.getOrCreateSession({
1232
+ merchantPublicKey,
1233
+ spendingLimitUsd: 25,
1234
+ // Default limit
1235
+ durationDays: 7
1236
+ });
1237
+ const headers = {
1238
+ "Content-Type": "application/json",
1239
+ "X-Mixr-Session": session.id,
1240
+ ...customHeaders
1241
+ };
1242
+ if (feature) {
1243
+ headers["X-Mixr-Feature"] = feature;
1244
+ }
1245
+ const requestBody = body !== void 0 ? typeof body === "string" ? body : JSON.stringify(body) : void 0;
1246
+ const response = await fetch(url, {
1247
+ method,
1248
+ headers,
1249
+ body: requestBody,
1250
+ signal: AbortSignal.timeout(this.timeout)
1251
+ });
1252
+ const chargedAmount = response.headers.get("X-Mixr-Charged");
1253
+ if (chargedAmount) {
1254
+ const amountUsd = parseFloat(chargedAmount);
1255
+ if (!isNaN(amountUsd)) {
1256
+ const payment = {
1257
+ amountUsd,
1258
+ recipient: merchantPublicKey,
1259
+ txHash: response.headers.get("X-Payment-TxHash"),
1260
+ timestamp: /* @__PURE__ */ new Date(),
1261
+ description: feature || "API call",
1262
+ url
1263
+ };
1264
+ this.payments.push(payment);
1265
+ this.totalSpentUsd += amountUsd;
1266
+ this.logger.payment(amountUsd, merchantPublicKey, feature);
1267
+ if (this.onPayment) {
1268
+ this.onPayment(payment);
1269
+ }
1270
+ }
1271
+ }
1272
+ if (response.status === 402) {
1273
+ const errorData = await response.json().catch(() => ({}));
1274
+ if (errorData.error === "session_expired") {
1275
+ this.logger.info("Session expired, creating new one...");
1276
+ const newSession = await this.getOrCreateSession({
1277
+ merchantPublicKey,
1278
+ spendingLimitUsd: 25,
1279
+ durationDays: 7
1280
+ });
1281
+ headers["X-Mixr-Session"] = newSession.id;
1282
+ return fetch(url, {
1283
+ method,
1284
+ headers,
1285
+ body: requestBody,
1286
+ signal: AbortSignal.timeout(this.timeout)
1287
+ });
1288
+ }
1289
+ if (errorData.error === "session_limit_exceeded") {
1290
+ throw new SpendingLimitExceededError(
1291
+ "session",
1292
+ errorData.sessionLimitUsd || 0,
1293
+ priceUsd || 0
1294
+ );
1295
+ }
1296
+ if (errorData.error === "insufficient_balance") {
1297
+ throw new InsufficientBalanceError(
1298
+ priceUsd || 0,
1299
+ errorData.availableUsd || 0
1300
+ );
1301
+ }
1302
+ }
1303
+ return response;
1304
+ }
1305
+ /**
1306
+ * Parse session response data into SessionAuthorization object.
1307
+ */
1308
+ parseSessionResponse(data) {
1309
+ return {
1310
+ id: data.id || data.session_id || data.sessionId,
1311
+ merchantId: data.merchantId || data.merchant_id,
1312
+ merchantName: data.merchantName || data.merchant_name || "Unknown",
1313
+ status: data.status || "active",
1314
+ spendingLimitUsd: Number(data.spendingLimitUsd || data.spending_limit_usd || data.spendingLimit || 0),
1315
+ amountUsedUsd: Number(data.amountUsedUsd || data.amount_used_usd || data.amountUsed || 0),
1316
+ remainingLimitUsd: Number(
1317
+ data.remainingLimitUsd || data.remaining_limit_usd || data.remainingLimit || Number(data.spendingLimitUsd || data.spending_limit_usd || 0) - Number(data.amountUsedUsd || data.amount_used_usd || 0)
1318
+ ),
1319
+ expiresAt: new Date(data.expiresAt || data.expires_at),
1320
+ createdAt: new Date(data.createdAt || data.created_at)
1321
+ };
1322
+ }
911
1323
  };
912
1324
  export {
913
1325
  AgentWallet,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mixrpay/agent-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "MixrPay Agent SDK - Enable AI agents to make x402 payments with session keys",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",