mppx 0.8.12 → 0.8.13

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.
Files changed (193) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +16 -1
  3. package/dist/cli/account.d.ts +88 -0
  4. package/dist/cli/account.d.ts.map +1 -1
  5. package/dist/cli/account.js +39 -9
  6. package/dist/cli/account.js.map +1 -1
  7. package/dist/cli/cli.d.ts +2 -1
  8. package/dist/cli/cli.d.ts.map +1 -1
  9. package/dist/cli/cli.js +59 -2
  10. package/dist/cli/cli.js.map +1 -1
  11. package/dist/cli/sessions/Manager.d.ts +31 -0
  12. package/dist/cli/sessions/Manager.d.ts.map +1 -0
  13. package/dist/cli/sessions/Manager.js +56 -0
  14. package/dist/cli/sessions/Manager.js.map +1 -0
  15. package/dist/cli/sessions/commands.d.ts +36 -0
  16. package/dist/cli/sessions/commands.d.ts.map +1 -0
  17. package/dist/cli/sessions/commands.js +407 -0
  18. package/dist/cli/sessions/commands.js.map +1 -0
  19. package/dist/cli/sessions/request.d.ts +35 -0
  20. package/dist/cli/sessions/request.d.ts.map +1 -0
  21. package/dist/cli/sessions/request.js +271 -0
  22. package/dist/cli/sessions/request.js.map +1 -0
  23. package/dist/cli/sessions/store.d.ts +134 -0
  24. package/dist/cli/sessions/store.d.ts.map +1 -0
  25. package/dist/cli/sessions/store.js +723 -0
  26. package/dist/cli/sessions/store.js.map +1 -0
  27. package/dist/cli/utils.d.ts +2 -0
  28. package/dist/cli/utils.d.ts.map +1 -1
  29. package/dist/cli/utils.js +12 -0
  30. package/dist/cli/utils.js.map +1 -1
  31. package/dist/cli/validate/index.d.ts.map +1 -1
  32. package/dist/cli/validate/index.js +12 -6
  33. package/dist/cli/validate/index.js.map +1 -1
  34. package/dist/cli/validate/messages.d.ts +2 -0
  35. package/dist/cli/validate/messages.d.ts.map +1 -0
  36. package/dist/cli/validate/messages.js +8 -0
  37. package/dist/cli/validate/messages.js.map +1 -0
  38. package/dist/client/node.d.ts +29 -0
  39. package/dist/client/node.d.ts.map +1 -0
  40. package/dist/client/node.js +177 -0
  41. package/dist/client/node.js.map +1 -0
  42. package/dist/internal/types.d.ts +24 -0
  43. package/dist/internal/types.d.ts.map +1 -1
  44. package/dist/stripe/server/Charge.d.ts +8 -4
  45. package/dist/stripe/server/Charge.d.ts.map +1 -1
  46. package/dist/stripe/server/Charge.js.map +1 -1
  47. package/dist/stripe/server/internal/html.gen.d.ts +1 -1
  48. package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
  49. package/dist/stripe/server/internal/html.gen.js +1 -1
  50. package/dist/stripe/server/internal/html.gen.js.map +1 -1
  51. package/dist/tempo/client/Methods.d.ts +1 -1
  52. package/dist/tempo/internal/types.d.ts +10 -1
  53. package/dist/tempo/internal/types.d.ts.map +1 -1
  54. package/dist/tempo/legacy/client/Session.d.ts +2 -2
  55. package/dist/tempo/legacy/server/Session.d.ts +3 -3
  56. package/dist/tempo/legacy/server/Session.d.ts.map +1 -1
  57. package/dist/tempo/legacy/server/Session.js.map +1 -1
  58. package/dist/tempo/server/Charge.d.ts +3 -2
  59. package/dist/tempo/server/Charge.d.ts.map +1 -1
  60. package/dist/tempo/server/Charge.js.map +1 -1
  61. package/dist/tempo/server/Methods.d.ts +2 -23
  62. package/dist/tempo/server/Methods.d.ts.map +1 -1
  63. package/dist/tempo/server/Methods.js +2 -2
  64. package/dist/tempo/server/Methods.js.map +1 -1
  65. package/dist/tempo/server/Subscription.d.ts +3 -2
  66. package/dist/tempo/server/Subscription.d.ts.map +1 -1
  67. package/dist/tempo/server/Subscription.js.map +1 -1
  68. package/dist/tempo/server/index.d.ts +1 -1
  69. package/dist/tempo/server/index.d.ts.map +1 -1
  70. package/dist/tempo/server/index.js.map +1 -1
  71. package/dist/tempo/server/internal/html.gen.d.ts +1 -1
  72. package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
  73. package/dist/tempo/server/internal/html.gen.js +1 -1
  74. package/dist/tempo/server/internal/html.gen.js.map +1 -1
  75. package/dist/tempo/session/Snapshot.d.ts +9 -0
  76. package/dist/tempo/session/Snapshot.d.ts.map +1 -1
  77. package/dist/tempo/session/Snapshot.js +6 -0
  78. package/dist/tempo/session/Snapshot.js.map +1 -1
  79. package/dist/tempo/session/client/ChannelOps.d.ts.map +1 -1
  80. package/dist/tempo/session/client/ChannelOps.js +11 -2
  81. package/dist/tempo/session/client/ChannelOps.js.map +1 -1
  82. package/dist/tempo/session/client/CredentialState.d.ts +21 -1
  83. package/dist/tempo/session/client/CredentialState.d.ts.map +1 -1
  84. package/dist/tempo/session/client/CredentialState.js +66 -1
  85. package/dist/tempo/session/client/CredentialState.js.map +1 -1
  86. package/dist/tempo/session/client/Session.d.ts +1 -1
  87. package/dist/tempo/session/client/SessionManager.d.ts +2 -2
  88. package/dist/tempo/session/client/SessionManager.d.ts.map +1 -1
  89. package/dist/tempo/session/client/SessionManager.js +128 -30
  90. package/dist/tempo/session/client/SessionManager.js.map +1 -1
  91. package/dist/tempo/session/client/Transports.d.ts +11 -2
  92. package/dist/tempo/session/client/Transports.d.ts.map +1 -1
  93. package/dist/tempo/session/client/Transports.js +33 -9
  94. package/dist/tempo/session/client/Transports.js.map +1 -1
  95. package/dist/tempo/session/client/internal/SessionManager.d.ts +19 -0
  96. package/dist/tempo/session/client/internal/SessionManager.d.ts.map +1 -0
  97. package/dist/tempo/session/client/internal/SessionManager.js +13 -0
  98. package/dist/tempo/session/client/internal/SessionManager.js.map +1 -0
  99. package/dist/tempo/session/precompile/Chain.js +13 -4
  100. package/dist/tempo/session/precompile/Chain.js.map +1 -1
  101. package/dist/tempo/session/server/CredentialVerification.d.ts +3 -0
  102. package/dist/tempo/session/server/CredentialVerification.d.ts.map +1 -1
  103. package/dist/tempo/session/server/CredentialVerification.js +14 -0
  104. package/dist/tempo/session/server/CredentialVerification.js.map +1 -1
  105. package/dist/tempo/session/server/MeteredStream.d.ts +7 -1
  106. package/dist/tempo/session/server/MeteredStream.d.ts.map +1 -1
  107. package/dist/tempo/session/server/MeteredStream.js +5 -4
  108. package/dist/tempo/session/server/MeteredStream.js.map +1 -1
  109. package/dist/tempo/session/server/RequestState.d.ts +5 -1
  110. package/dist/tempo/session/server/RequestState.d.ts.map +1 -1
  111. package/dist/tempo/session/server/RequestState.js +10 -4
  112. package/dist/tempo/session/server/RequestState.js.map +1 -1
  113. package/dist/tempo/session/server/Session.d.ts +21 -6
  114. package/dist/tempo/session/server/Session.d.ts.map +1 -1
  115. package/dist/tempo/session/server/Session.js +4 -1
  116. package/dist/tempo/session/server/Session.js.map +1 -1
  117. package/dist/tempo/session/server/Settlement.d.ts +20 -0
  118. package/dist/tempo/session/server/Settlement.d.ts.map +1 -1
  119. package/dist/tempo/session/server/Settlement.js +20 -0
  120. package/dist/tempo/session/server/Settlement.js.map +1 -1
  121. package/dist/tempo/session/server/Ws.d.ts +2 -0
  122. package/dist/tempo/session/server/Ws.d.ts.map +1 -1
  123. package/dist/tempo/session/server/Ws.js.map +1 -1
  124. package/dist/tempo/session/server/index.d.ts +1 -1
  125. package/dist/tempo/session/server/index.d.ts.map +1 -1
  126. package/dist/tempo/subscription/KeyAuthorization.d.ts +21 -21
  127. package/dist/validation/core.d.ts.map +1 -1
  128. package/dist/validation/core.js +6 -2
  129. package/dist/validation/core.js.map +1 -1
  130. package/dist/viem/Client.d.ts.map +1 -1
  131. package/dist/viem/Client.js +24 -17
  132. package/dist/viem/Client.js.map +1 -1
  133. package/package.json +6 -1
  134. package/src/cli/account.ts +45 -10
  135. package/src/cli/cli.test.ts +165 -70
  136. package/src/cli/cli.ts +67 -2
  137. package/src/cli/mcp.test.ts +11 -0
  138. package/src/cli/sessions/Manager.test.ts +249 -0
  139. package/src/cli/sessions/Manager.ts +93 -0
  140. package/src/cli/sessions/commands.ts +444 -0
  141. package/src/cli/sessions/request.test.ts +51 -0
  142. package/src/cli/sessions/request.ts +353 -0
  143. package/src/cli/sessions/store.test.ts +581 -0
  144. package/src/cli/sessions/store.ts +940 -0
  145. package/src/cli/utils.test.ts +23 -0
  146. package/src/cli/utils.ts +10 -0
  147. package/src/cli/validate/index.ts +12 -14
  148. package/src/cli/validate/messages.ts +7 -0
  149. package/src/cli/validate.test.ts +38 -5
  150. package/src/client/node.test.ts +115 -0
  151. package/src/client/node.ts +247 -0
  152. package/src/internal/types.test-d.ts +21 -0
  153. package/src/internal/types.ts +33 -0
  154. package/src/server/Mppx.test-d.ts +42 -0
  155. package/src/stripe/Methods.test.ts +10 -0
  156. package/src/stripe/server/Charge.test-d.ts +66 -0
  157. package/src/stripe/server/Charge.ts +8 -5
  158. package/src/stripe/server/internal/html.gen.ts +1 -1
  159. package/src/tempo/internal/types.ts +13 -2
  160. package/src/tempo/legacy/server/Defaults.test-d.ts +9 -0
  161. package/src/tempo/legacy/server/Session.ts +6 -5
  162. package/src/tempo/server/Charge.ts +6 -4
  163. package/src/tempo/server/Methods.ts +5 -4
  164. package/src/tempo/server/Subscription.ts +6 -4
  165. package/src/tempo/server/index.ts +5 -1
  166. package/src/tempo/server/internal/html.gen.ts +1 -1
  167. package/src/tempo/session/README.md +201 -0
  168. package/src/tempo/session/Snapshot.ts +21 -0
  169. package/src/tempo/session/client/ChannelOps.test.ts +40 -0
  170. package/src/tempo/session/client/ChannelOps.ts +12 -2
  171. package/src/tempo/session/client/CredentialState.test.ts +2 -2
  172. package/src/tempo/session/client/CredentialState.ts +103 -1
  173. package/src/tempo/session/client/Session.test.ts +3 -3
  174. package/src/tempo/session/client/SessionManager.test.ts +137 -4
  175. package/src/tempo/session/client/SessionManager.ts +139 -33
  176. package/src/tempo/session/client/Transports.test.ts +150 -8
  177. package/src/tempo/session/client/Transports.ts +48 -10
  178. package/src/tempo/session/client/internal/SessionManager.ts +36 -0
  179. package/src/tempo/session/precompile/Chain.test.ts +26 -0
  180. package/src/tempo/session/precompile/Chain.ts +17 -4
  181. package/src/tempo/session/server/CredentialVerification.ts +18 -1
  182. package/src/tempo/session/server/MeteredStream.ts +12 -5
  183. package/src/tempo/session/server/RequestState.test.ts +39 -3
  184. package/src/tempo/session/server/RequestState.ts +13 -6
  185. package/src/tempo/session/server/Session.test.ts +461 -0
  186. package/src/tempo/session/server/Session.ts +31 -8
  187. package/src/tempo/session/server/Settlement.ts +45 -0
  188. package/src/tempo/session/server/Ws.test.ts +113 -0
  189. package/src/tempo/session/server/Ws.ts +2 -0
  190. package/src/tempo/session/server/index.ts +2 -0
  191. package/src/validation/core.ts +7 -1
  192. package/src/viem/Client.test.ts +44 -1
  193. package/src/viem/Client.ts +31 -17
@@ -32,6 +32,7 @@ import cli from './cli.js'
32
32
  const testPrivateKey = generatePrivateKey()
33
33
  const testAccount = privateKeyToAccount(testPrivateKey)
34
34
  const cliTestXdgDataHome = fs.mkdtempSync(path.join(os.tmpdir(), 'mppx-cli-xdg-'))
35
+ let cliServeInvocation = 0
35
36
  const unfundedToken = '0x20c0000000000000000000000000000000000002' as Address
36
37
  const cliSessionFeePayerPolicy = {
37
38
  maxGas: 2_250_000n,
@@ -47,7 +48,13 @@ async function serve(argv: string[], options?: { env?: Record<string, string | u
47
48
  let stderr = ''
48
49
  let exitCode: number | undefined
49
50
  const saved: Record<string, string | undefined> = {}
50
- const env = { XDG_DATA_HOME: cliTestXdgDataHome, ...options?.env }
51
+ const invocationHome = path.join(cliTestXdgDataHome, `serve-${cliServeInvocation++}`)
52
+ const env = {
53
+ XDG_CONFIG_HOME: path.join(invocationHome, 'config'),
54
+ XDG_DATA_HOME: cliTestXdgDataHome,
55
+ XDG_STATE_HOME: path.join(invocationHome, 'state'),
56
+ ...options?.env,
57
+ }
51
58
  for (const [key, value] of Object.entries(env)) {
52
59
  saved[key] = process.env[key]
53
60
  if (value === undefined) delete process.env[key]
@@ -1049,10 +1056,10 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
1049
1056
 
1050
1057
  try {
1051
1058
  const { output } = await serve(
1052
- [httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'],
1059
+ [httpServer.url, '--rpc-url', rpcUrl, '--include', '-M', 'deposit=10'],
1053
1060
  { env: { MPPX_PRIVATE_KEY: testPrivateKey } },
1054
1061
  )
1055
- expect(output).toContain('scraped-content')
1062
+ expect(output).toBe('scraped-content')
1056
1063
  } finally {
1057
1064
  httpServer.close()
1058
1065
  }
@@ -1133,71 +1140,108 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
1133
1140
  }
1134
1141
  })
1135
1142
 
1136
- test('bug: non-SSE open should not double-charge tick amount', { timeout: 120_000 }, async () => {
1137
- await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
1138
- await fundAccount({ address: testAccount.address, token: asset })
1139
-
1140
- const escrow = tip20ChannelEscrow
1141
- const store = Store.memory()
1142
- const tickAmount = '0.001'
1143
- const server = Mppx_server.create({
1144
- methods: [
1145
- tempo.session({
1146
- account: accounts[0],
1147
- store,
1148
- getClient: () => client,
1149
- currency: asset,
1150
- escrowContract: escrow,
1151
- chainId: client.chain.id,
1152
- feePayer: true,
1153
- feePayerPolicy: cliSessionFeePayerPolicy,
1154
- }),
1155
- ],
1156
- realm: 'cli-test-double-charge',
1157
- secretKey: 'cli-test-secret-cli-test-secret-32',
1158
- })
1143
+ test(
1144
+ 'selects retained non-SSE channels with auto, explicit, and new modes',
1145
+ { timeout: 120_000 },
1146
+ async () => {
1147
+ await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
1148
+ await fundAccount({ address: testAccount.address, token: asset })
1159
1149
 
1160
- // Track voucher cumulative amounts from credential payloads
1161
- const voucherAmounts: string[] = []
1150
+ const escrow = tip20ChannelEscrow
1151
+ const store = Store.memory()
1152
+ const tickAmount = '0.001'
1153
+ const server = Mppx_server.create({
1154
+ methods: [
1155
+ tempo.session({
1156
+ account: accounts[0],
1157
+ store,
1158
+ getClient: () => client,
1159
+ currency: asset,
1160
+ escrowContract: escrow,
1161
+ chainId: client.chain.id,
1162
+ feePayer: true,
1163
+ feePayerPolicy: cliSessionFeePayerPolicy,
1164
+ }),
1165
+ ],
1166
+ realm: 'cli-test-double-charge',
1167
+ secretKey: 'cli-test-secret-cli-test-secret-32',
1168
+ })
1162
1169
 
1163
- const httpServer = await Http.createServer(async (req, res) => {
1164
- const authHeader = req.headers.authorization
1165
- if (authHeader) {
1166
- try {
1167
- const cred = Credential.deserialize<SessionCredentialPayload>(authHeader)
1168
- if (cred.payload.action === 'voucher' && 'cumulativeAmount' in cred.payload) {
1169
- voucherAmounts.push(cred.payload.cumulativeAmount)
1170
- }
1171
- } catch {}
1172
- }
1170
+ const voucherAmounts: string[] = []
1171
+ const credentials: { action: string; channelId: string }[] = []
1173
1172
 
1174
- const result = await toNodeListener(
1175
- server.session({
1176
- amount: tickAmount,
1177
- recipient: accounts[0].address,
1178
- unitType: 'page',
1179
- }),
1180
- )(req, res)
1181
- if (result.status === 402) return
1182
- // Non-SSE: plain text response (not text/event-stream)
1183
- res.end('scraped-content')
1184
- })
1173
+ const httpServer = await Http.createServer(async (req, res) => {
1174
+ const authHeader = req.headers.authorization
1175
+ if (authHeader) {
1176
+ try {
1177
+ const cred = Credential.deserialize<SessionCredentialPayload>(authHeader)
1178
+ credentials.push({
1179
+ action: cred.payload.action,
1180
+ channelId: cred.payload.channelId,
1181
+ })
1182
+ if (cred.payload.action === 'voucher' && 'cumulativeAmount' in cred.payload) {
1183
+ voucherAmounts.push(cred.payload.cumulativeAmount)
1184
+ }
1185
+ } catch {}
1186
+ }
1185
1187
 
1186
- try {
1187
- await serve([httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'], {
1188
- env: { MPPX_PRIVATE_KEY: testPrivateKey },
1188
+ const result = await toNodeListener(
1189
+ server.session({
1190
+ amount: tickAmount,
1191
+ recipient: accounts[0].address,
1192
+ unitType: 'page',
1193
+ }),
1194
+ )(req, res)
1195
+ if (result.status === 402) return
1196
+ // Non-SSE: plain text response (not text/event-stream)
1197
+ res.end('scraped-content')
1189
1198
  })
1190
1199
 
1191
- // No follow-up voucher should be sent after a non-SSE open.
1192
- // The open credential already paid for this unit, so the CLI
1193
- // should NOT send a redundant voucher that would double-charge.
1194
- expect(voucherAmounts.length).toBe(0)
1195
- } finally {
1196
- httpServer.close()
1197
- }
1198
- })
1200
+ try {
1201
+ const stateHome = fs.mkdtempSync(path.join(cliTestXdgDataHome, 'reuse-state-'))
1202
+ const env = { MPPX_PRIVATE_KEY: testPrivateKey, XDG_STATE_HOME: stateHome }
1203
+ const runRequest = async (argv: string[]) => {
1204
+ const result = await serve(argv, { env })
1205
+ expect(result.exitCode, `${result.output}\n${result.stderr}`).toBeUndefined()
1206
+ }
1207
+ await runRequest([httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'])
1208
+ const retainedChannelId = credentials.find(({ action }) => action === 'open')!.channelId
1209
+ await runRequest([httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'])
1210
+ await runRequest([
1211
+ httpServer.url,
1212
+ '--rpc-url',
1213
+ rpcUrl,
1214
+ '--session',
1215
+ retainedChannelId,
1216
+ '-s',
1217
+ '-M',
1218
+ 'deposit=10',
1219
+ ])
1220
+ await runRequest([
1221
+ httpServer.url,
1222
+ '--rpc-url',
1223
+ rpcUrl,
1224
+ '--session',
1225
+ 'new',
1226
+ '-s',
1227
+ '-M',
1228
+ 'deposit=10',
1229
+ ])
1230
+
1231
+ const payments = credentials.filter(({ action }) => ['open', 'voucher'].includes(action))
1232
+ expect(voucherAmounts).toEqual(['2000', '3000'])
1233
+ expect(payments.map(({ action }) => action)).toEqual(['open', 'voucher', 'voucher', 'open'])
1234
+ expect(new Set(payments.slice(0, 3).map(({ channelId }) => channelId))).toEqual(
1235
+ new Set([retainedChannelId]),
1236
+ )
1237
+ expect(payments[3]?.channelId).not.toBe(retainedChannelId)
1238
+ } finally {
1239
+ httpServer.close()
1240
+ }
1241
+ },
1242
+ )
1199
1243
 
1200
- test('bug: closeChannel sends action "close" not "voucher"', { timeout: 120_000 }, async () => {
1244
+ test('retains a session until `sessions close` is invoked', { timeout: 120_000 }, async () => {
1201
1245
  await fundAccount({ address: testAccount.address, token: Addresses.pathUsd })
1202
1246
  await fundAccount({ address: testAccount.address, token: asset })
1203
1247
 
@@ -1212,7 +1256,7 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
1212
1256
  currency: asset,
1213
1257
  escrowContract: escrow,
1214
1258
  chainId: client.chain.id,
1215
- feePayer: true,
1259
+ feePayer: accounts[1],
1216
1260
  feePayerPolicy: cliSessionFeePayerPolicy,
1217
1261
  }),
1218
1262
  ],
@@ -1220,8 +1264,8 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
1220
1264
  secretKey: 'cli-test-secret-cli-test-secret-32',
1221
1265
  })
1222
1266
 
1223
- // Track the credential payload action from the close request
1224
1267
  const credentialActions: string[] = []
1268
+ let channelId: string | undefined
1225
1269
 
1226
1270
  const httpServer = await Http.createServer(async (req, res) => {
1227
1271
  // Capture credential action from every request with Authorization header
@@ -1230,6 +1274,7 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
1230
1274
  try {
1231
1275
  const cred = Credential.deserialize<SessionCredentialPayload>(authHeader)
1232
1276
  credentialActions.push(cred.payload.action)
1277
+ channelId = cred.payload.channelId
1233
1278
  } catch {}
1234
1279
  }
1235
1280
 
@@ -1245,13 +1290,31 @@ describe('session multi-fetch (examples/session/multi-fetch)', () => {
1245
1290
  })
1246
1291
 
1247
1292
  try {
1293
+ const stateHome = fs.mkdtempSync(path.join(cliTestXdgDataHome, 'close-state-'))
1294
+ const env = { MPPX_PRIVATE_KEY: testPrivateKey, XDG_STATE_HOME: stateHome }
1248
1295
  await serve([httpServer.url, '--rpc-url', rpcUrl, '-s', '-M', 'deposit=10'], {
1249
- env: { MPPX_PRIVATE_KEY: testPrivateKey },
1296
+ env,
1297
+ })
1298
+
1299
+ expect(credentialActions).toEqual(['open'])
1300
+ expect(channelId).toMatch(/^0x[0-9a-f]{64}$/)
1301
+
1302
+ const listed = await serve(['sessions', 'list', '--json'], { env })
1303
+ expect(JSON.parse(listed.output).sessions).toEqual([
1304
+ expect.objectContaining({ channelId, status: 'open' }),
1305
+ ])
1306
+
1307
+ const closed = await serve(['sessions', 'close', channelId!, '--rpc-url', rpcUrl, '--json'], {
1308
+ env,
1250
1309
  })
1310
+ expect(closed.exitCode, `${closed.output}\n${closed.stderr}`).toBeUndefined()
1311
+ expect(JSON.parse(closed.output)).toEqual(
1312
+ expect.objectContaining({ channelId, status: 'closed' }),
1313
+ )
1314
+ expect(credentialActions.at(-1)).toBe('close')
1251
1315
 
1252
- // The last credential sent should be the close request with action: 'close'
1253
- const lastAction = credentialActions[credentialActions.length - 1]
1254
- expect(lastAction).toBe('close')
1316
+ const empty = await serve(['sessions', 'list', '--json'], { env })
1317
+ expect(JSON.parse(empty.output)).toEqual({ sessions: [] })
1255
1318
  } finally {
1256
1319
  httpServer.close()
1257
1320
  }
@@ -1298,7 +1361,9 @@ describe('session sse (examples/session/sse)', () => {
1298
1361
  secretKey: 'cli-test-secret-cli-test-secret-32',
1299
1362
  })
1300
1363
 
1364
+ const paidAcceptHeaders: (string | undefined)[] = []
1301
1365
  const httpServer = await Http.createServer(async (req, res) => {
1366
+ if (req.headers.authorization) paidAcceptHeaders.push(req.headers.accept)
1302
1367
  const result = await toNodeListener(
1303
1368
  server.session({
1304
1369
  amount: '0.001',
@@ -1322,10 +1387,12 @@ describe('session sse (examples/session/sse)', () => {
1322
1387
  })
1323
1388
 
1324
1389
  try {
1325
- const { output } = await serve([httpServer.url, '--rpc-url', rpcUrl, '-M', 'deposit=10'], {
1326
- env: { MPPX_PRIVATE_KEY: testPrivateKey },
1327
- })
1390
+ const { output } = await serve(
1391
+ [httpServer.url, '--rpc-url', rpcUrl, '-H', 'Accept: application/json', '-M', 'deposit=10'],
1392
+ { env: { MPPX_PRIVATE_KEY: testPrivateKey } },
1393
+ )
1328
1394
  expect(output.trim()).toBe('Hello world!')
1395
+ expect(paidAcceptHeaders).toEqual(['application/json'])
1329
1396
  } finally {
1330
1397
  httpServer.close()
1331
1398
  }
@@ -1836,8 +1903,36 @@ test('mppx --help', async () => {
1836
1903
  expect(output).toContain('mppx')
1837
1904
  expect(output).toContain('<url>')
1838
1905
  expect(output).toContain('account')
1906
+ expect(output).toContain('sessions')
1839
1907
  expect(output).toContain('services')
1840
1908
  expect(output).toContain('sign')
1909
+ expect(output).toContain('--session')
1910
+ })
1911
+
1912
+ test('mppx sessions --help', async () => {
1913
+ const { output } = await serve(['sessions', '--help'])
1914
+ expect(output).toContain('close Cooperatively close')
1915
+ expect(output).toContain('list List persistent')
1916
+ expect(output).toContain('view View a persistent')
1917
+ })
1918
+
1919
+ test('mppx sessions close --help', async () => {
1920
+ const { output } = await serve(['sessions', 'close', '--help'])
1921
+ expect(output).toContain('--all')
1922
+ expect(output).toContain('--yes')
1923
+ expect(output).toContain('--url')
1924
+ })
1925
+
1926
+ test('mppx sessions close --all requires confirmation', async () => {
1927
+ const { exitCode, output } = await serve(['sessions', 'close', '--all'])
1928
+ expect(exitCode).toBe(2)
1929
+ expect(output).toContain('requires --yes')
1930
+ })
1931
+
1932
+ test('mppx sessions close --all returns an empty structured result', async () => {
1933
+ const { exitCode, output } = await serve(['sessions', 'close', '--all', '--yes', '--json'])
1934
+ expect(exitCode).toBeUndefined()
1935
+ expect(JSON.parse(output)).toEqual({ closed: [], failed: [] })
1841
1936
  })
1842
1937
 
1843
1938
  describe('account fund help', () => {
package/src/cli/cli.ts CHANGED
@@ -13,6 +13,7 @@ import { normalizeHeaders } from '../client/internal/Fetch.js'
13
13
  import * as Mppx from '../client/Mppx.js'
14
14
  import * as Constants from '../Constants.js'
15
15
  import { validate as validateDiscovery } from '../discovery/Validate.js'
16
+ import { isTempoSessionChallenge } from '../tempo/session/client/Transports.js'
16
17
  import { createDefaultStore, createKeychain, resolveAccountName } from './account.js'
17
18
  import { loadConfig, resolveAcceptPayment, selectChallenge } from './internal.js'
18
19
  import type { Plugin } from './plugins/plugin.js'
@@ -21,6 +22,9 @@ import {
21
22
  readTempoKeystore,
22
23
  resolveTempoAccount,
23
24
  } from './plugins/tempo.js'
25
+ import sessions, { sessionCommandError } from './sessions/commands.js'
26
+ import { runPersistentSessionRequest } from './sessions/request.js'
27
+ import { createSessionRegistry } from './sessions/store.js'
24
28
  import {
25
29
  chainName,
26
30
  confirm,
@@ -244,6 +248,15 @@ const cli = Cli.create('mppx', {
244
248
  .string()
245
249
  .optional()
246
250
  .describe('RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)'),
251
+ session: z
252
+ .string()
253
+ .optional()
254
+ .default('auto')
255
+ .refine(
256
+ (value) => value === 'auto' || value === 'new' || /^0x[0-9a-fA-F]{64}$/.test(value),
257
+ 'Expected auto, new, or a 32-byte channel ID',
258
+ )
259
+ .describe('Session selection: auto, new, or channel ID'),
247
260
  silent: z.boolean().default(false).describe('Silent mode (suppress progress and info)'),
248
261
  slippage: z.number().optional().describe('Tempo auto-swap max slippage percentage'),
249
262
  userAgent: z
@@ -347,6 +360,9 @@ const cli = Cli.create('mppx', {
347
360
  }
348
361
 
349
362
  try {
363
+ const methodOpts = parseMethodOpts(c.options.methodOpt)
364
+ const sessionRegistry = createSessionRegistry()
365
+
350
366
  const init: RequestInit = { redirect: c.options.location ? 'follow' : 'manual' }
351
367
  if (c.options.jsonBody) {
352
368
  init.body = c.options.jsonBody
@@ -368,6 +384,12 @@ const cli = Cli.create('mppx', {
368
384
  if (c.options.verbose >= 2) printRequestHeaders(url, init, info)
369
385
  const challengeResponse = await targetFetch(fetchUrl, init)
370
386
  if (challengeResponse.status !== 402) {
387
+ if (c.options.session !== 'auto')
388
+ return c.error({
389
+ code: 'UNSUPPORTED_SESSION',
390
+ message: '--session requires a tempo/session payment challenge.',
391
+ exitCode: 2,
392
+ })
371
393
  if (c.options.fail && challengeResponse.status >= 400)
372
394
  return c.error({
373
395
  code: 'HTTP_ERROR',
@@ -379,7 +401,6 @@ const cli = Cli.create('mppx', {
379
401
  return
380
402
  }
381
403
 
382
- const methodOpts = parseMethodOpts(c.options.methodOpt)
383
404
  const offeredChallenges = Challenge.fromResponseList(challengeResponse)
384
405
  const currencyChallenges = filterChallengesByCurrency(offeredChallenges, c.options.currency)
385
406
  if (c.options.currency && currencyChallenges.length === 0) {
@@ -446,7 +467,12 @@ const cli = Cli.create('mppx', {
446
467
  // Display challenge
447
468
  const shownKeys = new Set<string>()
448
469
  {
449
- printResponseHeaders(challengeResponse, headerOpts)
470
+ printResponseHeaders(
471
+ challengeResponse,
472
+ challenge.method === 'tempo' && challenge.intent === 'session' && c.options.include
473
+ ? { ...headerOpts, include: false, verbose: Math.max(headerOpts.verbose, 2) }
474
+ : headerOpts,
475
+ )
450
476
 
451
477
  const challengeRows = (() => {
452
478
  const skip = new Set(['id', 'request'])
@@ -517,6 +543,44 @@ const cli = Cli.create('mppx', {
517
543
  }
518
544
  }
519
545
 
546
+ const persistentSessionAccount =
547
+ process.env.MPPX_PRIVATE_KEY?.trim() ||
548
+ !isTempoAccount(resolveAccountName(c.options.account))
549
+ if (isTempoSessionChallenge(challenge) && persistentSessionAccount) {
550
+ try {
551
+ await runPersistentSessionRequest({
552
+ challenge,
553
+ challengeResponse: selectedChallengeResponse,
554
+ endpoint: url,
555
+ fetch: targetFetch,
556
+ fetchInput: fetchUrl,
557
+ init,
558
+ info,
559
+ methodOptions: methodOpts,
560
+ options: {
561
+ account: c.options.account,
562
+ fail: c.options.fail,
563
+ include: c.options.include,
564
+ network: c.options.network,
565
+ rpcUrl: c.options.rpcUrl,
566
+ session: c.options.session,
567
+ silent: c.options.silent,
568
+ verbose: c.options.verbose,
569
+ },
570
+ registry: sessionRegistry,
571
+ })
572
+ } catch (error) {
573
+ return sessionCommandError(error, 'SESSION_REQUEST_FAILED')
574
+ }
575
+ return
576
+ }
577
+ if (c.options.session !== 'auto')
578
+ return c.error({
579
+ code: 'UNSUPPORTED_SESSION',
580
+ message: '--session requires a tempo/session payment challenge.',
581
+ exitCode: 2,
582
+ })
583
+
520
584
  // Create credential
521
585
  let credential: string
522
586
  if (pluginResult?.createCredential)
@@ -1578,6 +1642,7 @@ cli.command(account)
1578
1642
  cli.command(discover)
1579
1643
  cli.command(init)
1580
1644
  cli.command(services)
1645
+ cli.command(sessions)
1581
1646
  cli.command(sign)
1582
1647
  cli.command(validate)
1583
1648
 
@@ -193,6 +193,9 @@ test('tools/list exposes mppx commands with input and output schemas', async ()
193
193
  'services_endpoints',
194
194
  'services_list',
195
195
  'services_show',
196
+ 'sessions_close',
197
+ 'sessions_list',
198
+ 'sessions_view',
196
199
  'sign',
197
200
  'validate',
198
201
  ])
@@ -208,6 +211,14 @@ test('tools/list exposes mppx commands with input and output schemas', async ()
208
211
  type: 'object',
209
212
  }),
210
213
  )
214
+ expect(
215
+ tools.find((tool: { name: string }) => tool.name === 'sessions_list').outputSchema,
216
+ ).toEqual(
217
+ expect.objectContaining({
218
+ properties: expect.objectContaining({ sessions: expect.any(Object) }),
219
+ type: 'object',
220
+ }),
221
+ )
211
222
  expect(client.nonJsonLines).toEqual([])
212
223
  })
213
224