mppx 0.5.7 → 0.5.9
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/CHANGELOG.md +14 -0
- package/dist/Challenge.d.ts +3 -2
- package/dist/Challenge.d.ts.map +1 -1
- package/dist/Challenge.js +27 -9
- package/dist/Challenge.js.map +1 -1
- package/dist/Method.d.ts +32 -14
- package/dist/Method.d.ts.map +1 -1
- package/dist/Method.js.map +1 -1
- package/dist/Store.d.ts +68 -2
- package/dist/Store.d.ts.map +1 -1
- package/dist/Store.js +41 -4
- package/dist/Store.js.map +1 -1
- package/dist/mcp-sdk/server/Transport.d.ts.map +1 -1
- package/dist/mcp-sdk/server/Transport.js +7 -0
- package/dist/mcp-sdk/server/Transport.js.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +133 -70
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts +8 -2
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +26 -1
- package/dist/server/Transport.js.map +1 -1
- package/dist/tempo/client/SessionManager.d.ts +13 -2
- package/dist/tempo/client/SessionManager.d.ts.map +1 -1
- package/dist/tempo/client/SessionManager.js +429 -4
- package/dist/tempo/client/SessionManager.js.map +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +28 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +89 -0
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +4 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +90 -66
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +3 -0
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +3 -0
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +8 -2
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/server/index.d.ts +1 -0
- package/dist/tempo/server/index.d.ts.map +1 -1
- package/dist/tempo/server/index.js +1 -0
- package/dist/tempo/server/index.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/internal/transport.d.ts.map +1 -1
- package/dist/tempo/server/internal/transport.js +16 -6
- package/dist/tempo/server/internal/transport.js.map +1 -1
- package/dist/tempo/session/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/ChannelStore.d.ts.map +1 -1
- package/dist/tempo/session/ChannelStore.js +55 -14
- package/dist/tempo/session/ChannelStore.js.map +1 -1
- package/dist/tempo/session/Sse.d.ts +11 -2
- package/dist/tempo/session/Sse.d.ts.map +1 -1
- package/dist/tempo/session/Sse.js +66 -25
- package/dist/tempo/session/Sse.js.map +1 -1
- package/dist/tempo/session/Ws.d.ts +87 -0
- package/dist/tempo/session/Ws.d.ts.map +1 -0
- package/dist/tempo/session/Ws.js +428 -0
- package/dist/tempo/session/Ws.js.map +1 -0
- package/dist/tempo/session/index.d.ts +1 -0
- package/dist/tempo/session/index.d.ts.map +1 -1
- package/dist/tempo/session/index.js +1 -0
- package/dist/tempo/session/index.js.map +1 -1
- package/package.json +2 -2
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +28 -9
- package/src/Method.ts +61 -20
- package/src/Store.test-d.ts +80 -2
- package/src/Store.test.ts +150 -13
- package/src/Store.ts +140 -3
- package/src/mcp-sdk/server/Transport.test.ts +12 -0
- package/src/mcp-sdk/server/Transport.ts +8 -0
- package/src/server/Mppx.test.ts +105 -0
- package/src/server/Mppx.ts +178 -88
- package/src/server/Transport.test.ts +31 -0
- package/src/server/Transport.ts +31 -2
- package/src/tempo/client/SessionManager.ts +510 -7
- package/src/tempo/internal/fee-payer.test.ts +115 -1
- package/src/tempo/internal/fee-payer.ts +138 -1
- package/src/tempo/server/AtomicStore.test-d.ts +34 -0
- package/src/tempo/server/Charge.test.ts +128 -0
- package/src/tempo/server/Charge.ts +118 -93
- package/src/tempo/server/Methods.ts +3 -0
- package/src/tempo/server/Session.test.ts +1044 -47
- package/src/tempo/server/Session.ts +8 -2
- package/src/tempo/server/Sse.test.ts +29 -0
- package/src/tempo/server/index.ts +1 -0
- package/src/tempo/server/internal/html/main.ts +9 -10
- package/src/tempo/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/internal/transport.ts +19 -6
- package/src/tempo/session/ChannelStore.test.ts +20 -1
- package/src/tempo/session/ChannelStore.ts +77 -14
- package/src/tempo/session/Sse.ts +77 -24
- package/src/tempo/session/Ws.test.ts +410 -0
- package/src/tempo/session/Ws.ts +563 -0
- package/src/tempo/session/index.ts +1 -0
|
@@ -62,8 +62,8 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
62
62
|
memo,
|
|
63
63
|
waitForConfirmation = true,
|
|
64
64
|
} = parameters
|
|
65
|
-
const store = (parameters.store ?? Store.memory()) as Store.
|
|
66
|
-
const proofStore = parameters.store as Store.
|
|
65
|
+
const store = (parameters.store ?? Store.memory()) as Store.AtomicStore<charge.StoreItemMap>
|
|
66
|
+
const proofStore = parameters.store as Store.AtomicStore<charge.StoreItemMap> | undefined
|
|
67
67
|
|
|
68
68
|
const { recipient, feePayer, feePayerUrl } = Account.resolve(parameters)
|
|
69
69
|
|
|
@@ -154,7 +154,7 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
154
154
|
const { challenge } = credential
|
|
155
155
|
const resolvedRequest = Methods.charge.schema.request.parse(request)
|
|
156
156
|
const chainId = resolvedRequest.methodDetails?.chainId ?? request.chainId
|
|
157
|
-
const feePayer = request.feePayer
|
|
157
|
+
const feePayer = typeof request.feePayer === 'object' ? request.feePayer : undefined
|
|
158
158
|
|
|
159
159
|
const client = await getClient({ chainId })
|
|
160
160
|
|
|
@@ -177,7 +177,9 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
177
177
|
switch (payload.type) {
|
|
178
178
|
case 'hash': {
|
|
179
179
|
const hash = payload.hash as `0x${string}`
|
|
180
|
-
await
|
|
180
|
+
if (!(await markHashUsed(store, hash))) {
|
|
181
|
+
throw new VerificationFailedError({ reason: 'Transaction hash has already been used' })
|
|
182
|
+
}
|
|
181
183
|
|
|
182
184
|
const expectedTransfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
|
|
183
185
|
const receipt = await getTransactionReceipt(client, { hash })
|
|
@@ -186,7 +188,6 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
186
188
|
sender: receipt.from,
|
|
187
189
|
transfers: expectedTransfers,
|
|
188
190
|
})
|
|
189
|
-
|
|
190
191
|
// Only verify challenge binding when using auto-generated attribution memos.
|
|
191
192
|
// Explicit memos (set by the server) are strictly matched by assertTransferLogs
|
|
192
193
|
// but are NOT challenge-bound — callers that set explicit memos are responsible
|
|
@@ -197,8 +198,6 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
197
198
|
realm: challenge.realm,
|
|
198
199
|
})
|
|
199
200
|
|
|
200
|
-
await markHashUsed(store, hash)
|
|
201
|
-
|
|
202
201
|
return toReceipt(receipt)
|
|
203
202
|
}
|
|
204
203
|
|
|
@@ -230,9 +229,8 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
230
229
|
})
|
|
231
230
|
if (!valid) throw new MismatchError('Proof signature does not match source.', {})
|
|
232
231
|
|
|
233
|
-
if (proofStore) {
|
|
234
|
-
|
|
235
|
-
await markProofUsed(proofStore, challenge.id)
|
|
232
|
+
if (proofStore && !(await markProofUsed(proofStore, challenge.id))) {
|
|
233
|
+
throw new VerificationFailedError({ reason: 'Proof credential has already been used' })
|
|
236
234
|
}
|
|
237
235
|
|
|
238
236
|
return {
|
|
@@ -248,64 +246,80 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
248
246
|
|
|
249
247
|
// Pre-broadcast dedup: catch exact byte-for-byte replays early.
|
|
250
248
|
const hash = keccak256(serializedTransaction)
|
|
251
|
-
await
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!FeePayer.isTempoTransaction(serializedTransaction))
|
|
255
|
-
throw new MismatchError('Only Tempo (0x76/0x78) transactions are supported.', {})
|
|
256
|
-
|
|
257
|
-
const transaction = Transaction.deserialize(serializedTransaction)
|
|
258
|
-
if (!transaction.signature || !transaction.from)
|
|
259
|
-
throw new MismatchError(
|
|
260
|
-
'Transaction must be signed by the sender before fee payer co-signing.',
|
|
261
|
-
{},
|
|
262
|
-
)
|
|
249
|
+
if (!(await markHashUsed(store, hash))) {
|
|
250
|
+
throw new VerificationFailedError({ reason: 'Transaction hash has already been used' })
|
|
251
|
+
}
|
|
263
252
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
253
|
+
let releaseReservation = true
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
if (!FeePayer.isTempoTransaction(serializedTransaction))
|
|
257
|
+
throw new MismatchError('Only Tempo (0x76/0x78) transactions are supported.', {})
|
|
258
|
+
|
|
259
|
+
const transaction = Transaction.deserialize(serializedTransaction)
|
|
260
|
+
if (!transaction.signature || !transaction.from)
|
|
261
|
+
throw new MismatchError(
|
|
262
|
+
'Transaction must be signed by the sender before fee payer co-signing.',
|
|
263
|
+
{},
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
const calls = (transaction.calls ?? []) as readonly {
|
|
267
|
+
data?: `0x${string}` | undefined
|
|
268
|
+
to?: `0x${string}` | undefined
|
|
269
|
+
}[]
|
|
270
|
+
const transfers = getExpectedTransfers({ amount, memo, methodDetails, recipient })
|
|
271
|
+
const isFeePayerTx = !!(feePayer || feePayerUrl) && methodDetails?.feePayer !== false
|
|
272
|
+
assertTransferCalls(calls, { currency, exactCount: isFeePayerTx, transfers })
|
|
273
|
+
|
|
274
|
+
if (isFeePayerTx)
|
|
275
|
+
FeePayer.validateCalls(transaction.calls, { amount, currency, recipient })
|
|
276
|
+
|
|
277
|
+
const expectedFeeToken = defaults.currency[chainId as keyof typeof defaults.currency]
|
|
278
|
+
const resolvedFeeToken = transaction.feeToken ?? expectedFeeToken
|
|
279
|
+
|
|
280
|
+
const serializedTransaction_final = await (async () => {
|
|
281
|
+
if (feePayer && methodDetails?.feePayer !== false) {
|
|
282
|
+
const sponsored = FeePayer.prepareSponsoredTransaction({
|
|
283
|
+
account: feePayer,
|
|
284
|
+
challengeExpires: expires,
|
|
285
|
+
chainId: chainId ?? client.chain!.id,
|
|
286
|
+
details: { amount, currency, recipient },
|
|
287
|
+
expectedFeeToken,
|
|
288
|
+
transaction: {
|
|
289
|
+
...transaction,
|
|
290
|
+
...(resolvedFeeToken ? { feeToken: resolvedFeeToken } : {}),
|
|
291
|
+
},
|
|
292
|
+
})
|
|
293
|
+
return signTransaction(client, sponsored as never)
|
|
294
|
+
}
|
|
295
|
+
return serializedTransaction
|
|
296
|
+
})()
|
|
297
|
+
|
|
298
|
+
if (waitForConfirmation) {
|
|
299
|
+
const receipt = await sendRawTransactionSync(client, {
|
|
300
|
+
serializedTransaction: serializedTransaction_final,
|
|
301
|
+
})
|
|
302
|
+
assertTransferLogs(receipt, {
|
|
303
|
+
currency,
|
|
304
|
+
sender: transaction.from! as `0x${string}`,
|
|
305
|
+
transfers,
|
|
306
|
+
})
|
|
307
|
+
// Post-broadcast dedup: catch malleable input variants
|
|
308
|
+
// (different serialized bytes, same underlying tx) that
|
|
309
|
+
// bypass the pre-broadcast check. Skip if the broadcast
|
|
310
|
+
// hash matches the input hash (already stored above).
|
|
311
|
+
if (
|
|
312
|
+
receipt.transactionHash.toLowerCase() !== hash.toLowerCase() &&
|
|
313
|
+
!(await markHashUsed(store, receipt.transactionHash))
|
|
314
|
+
) {
|
|
315
|
+
throw new VerificationFailedError({
|
|
316
|
+
reason: 'Transaction hash has already been used',
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
releaseReservation = false
|
|
320
|
+
return toReceipt(receipt)
|
|
286
321
|
}
|
|
287
|
-
return serializedTransaction
|
|
288
|
-
})()
|
|
289
322
|
|
|
290
|
-
if (waitForConfirmation) {
|
|
291
|
-
const receipt = await sendRawTransactionSync(client, {
|
|
292
|
-
serializedTransaction: serializedTransaction_final,
|
|
293
|
-
})
|
|
294
|
-
assertTransferLogs(receipt, {
|
|
295
|
-
currency,
|
|
296
|
-
sender: transaction.from! as `0x${string}`,
|
|
297
|
-
transfers,
|
|
298
|
-
})
|
|
299
|
-
// Post-broadcast dedup: catch malleable input variants
|
|
300
|
-
// (different serialized bytes, same underlying tx) that
|
|
301
|
-
// bypass the pre-broadcast check. Skip if the broadcast
|
|
302
|
-
// hash matches the input hash (already stored above).
|
|
303
|
-
if (receipt.transactionHash.toLowerCase() !== hash.toLowerCase()) {
|
|
304
|
-
await assertHashUnused(store, receipt.transactionHash)
|
|
305
|
-
await markHashUsed(store, receipt.transactionHash)
|
|
306
|
-
}
|
|
307
|
-
return toReceipt(receipt)
|
|
308
|
-
} else {
|
|
309
323
|
// Optimistic path: simulate to catch obvious reverts, then broadcast
|
|
310
324
|
// without waiting for on-chain confirmation. The returned receipt
|
|
311
325
|
// assumes success — callers opt into this risk via waitForConfirmation: false.
|
|
@@ -319,16 +333,24 @@ export function charge<const parameters extends charge.Parameters>(
|
|
|
319
333
|
serializedTransaction: serializedTransaction_final,
|
|
320
334
|
})
|
|
321
335
|
// Post-broadcast dedup: same
|
|
322
|
-
if (
|
|
323
|
-
|
|
324
|
-
await markHashUsed(store, reference)
|
|
336
|
+
if (
|
|
337
|
+
reference.toLowerCase() !== hash.toLowerCase() &&
|
|
338
|
+
!(await markHashUsed(store, reference))
|
|
339
|
+
) {
|
|
340
|
+
throw new VerificationFailedError({
|
|
341
|
+
reason: 'Transaction hash has already been used',
|
|
342
|
+
})
|
|
325
343
|
}
|
|
344
|
+
releaseReservation = false
|
|
326
345
|
return {
|
|
327
346
|
method: 'tempo',
|
|
328
347
|
status: 'success',
|
|
329
348
|
timestamp: new Date().toISOString(),
|
|
330
349
|
reference,
|
|
331
350
|
} as const
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (releaseReservation) await releaseHashUse(store, hash)
|
|
353
|
+
throw error
|
|
332
354
|
}
|
|
333
355
|
}
|
|
334
356
|
|
|
@@ -357,10 +379,13 @@ export declare namespace charge {
|
|
|
357
379
|
* is explicitly provided; otherwise proofs remain reusable until the
|
|
358
380
|
* challenge expires.
|
|
359
381
|
*
|
|
382
|
+
* Replay protection requires a {@link Store.AtomicStore} so replay markers
|
|
383
|
+
* can be written atomically.
|
|
384
|
+
*
|
|
360
385
|
* Use a shared store in multi-instance deployments so consumed hashes and
|
|
361
386
|
* proofs are visible across all server instances.
|
|
362
387
|
*/
|
|
363
|
-
store?: Store.
|
|
388
|
+
store?: Store.AtomicStore | undefined
|
|
364
389
|
/**
|
|
365
390
|
* Whether to wait for the charge transaction to confirm on-chain before
|
|
366
391
|
* responding. @default true
|
|
@@ -597,40 +622,33 @@ function getProofStoreKey(challengeId: string): `mppx:charge:${string}` {
|
|
|
597
622
|
return `mppx:charge:proof:${challengeId}`
|
|
598
623
|
}
|
|
599
624
|
|
|
600
|
-
/** @internal */
|
|
601
|
-
async function assertHashUnused(
|
|
602
|
-
store: Store.Store<charge.StoreItemMap>,
|
|
603
|
-
hash: `0x${string}`,
|
|
604
|
-
): Promise<void> {
|
|
605
|
-
const seen = await store.get(getHashStoreKey(hash))
|
|
606
|
-
if (seen !== null)
|
|
607
|
-
throw new VerificationFailedError({ reason: 'Transaction hash has already been used' })
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/** @internal */
|
|
611
625
|
async function markHashUsed(
|
|
612
|
-
store: Store.
|
|
626
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
613
627
|
hash: `0x${string}`,
|
|
614
|
-
): Promise<
|
|
615
|
-
|
|
628
|
+
): Promise<boolean> {
|
|
629
|
+
return store.update(getHashStoreKey(hash), (current) => {
|
|
630
|
+
if (current !== null) return { op: 'noop', result: false }
|
|
631
|
+
return { op: 'set', value: Date.now(), result: true }
|
|
632
|
+
})
|
|
616
633
|
}
|
|
617
634
|
|
|
618
635
|
/** @internal */
|
|
619
|
-
async function
|
|
620
|
-
store: Store.
|
|
621
|
-
|
|
636
|
+
async function releaseHashUse(
|
|
637
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
638
|
+
hash: `0x${string}`,
|
|
622
639
|
): Promise<void> {
|
|
623
|
-
|
|
624
|
-
if (seen !== null)
|
|
625
|
-
throw new VerificationFailedError({ reason: 'Proof credential has already been used' })
|
|
640
|
+
await store.delete(getHashStoreKey(hash))
|
|
626
641
|
}
|
|
627
642
|
|
|
628
643
|
/** @internal */
|
|
629
644
|
async function markProofUsed(
|
|
630
|
-
store: Store.
|
|
645
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
631
646
|
challengeId: string,
|
|
632
|
-
): Promise<
|
|
633
|
-
|
|
647
|
+
): Promise<boolean> {
|
|
648
|
+
return store.update(getProofStoreKey(challengeId), (current) => {
|
|
649
|
+
if (current !== null) return { op: 'noop', result: false }
|
|
650
|
+
return { op: 'set', value: Date.now(), result: true }
|
|
651
|
+
})
|
|
634
652
|
}
|
|
635
653
|
|
|
636
654
|
/** @internal */
|
|
@@ -676,6 +694,13 @@ class MismatchError extends PaymentError {
|
|
|
676
694
|
readonly type = 'https://paymentauth.org/problems/verification-failed'
|
|
677
695
|
|
|
678
696
|
constructor(reason: string, details: Record<string, string>) {
|
|
679
|
-
super(
|
|
697
|
+
super(
|
|
698
|
+
[
|
|
699
|
+
reason.startsWith('Payment verification failed')
|
|
700
|
+
? reason
|
|
701
|
+
: `Payment verification failed: ${reason}`,
|
|
702
|
+
...Object.entries(details).map(([k, v]) => ` - ${k}: ${v}`),
|
|
703
|
+
].join('\n'),
|
|
704
|
+
)
|
|
680
705
|
}
|
|
681
706
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Ws_ from '../session/Ws.js'
|
|
1
2
|
import { charge as charge_ } from './Charge.js'
|
|
2
3
|
import { session as session_, settle as settle_ } from './Session.js'
|
|
3
4
|
|
|
@@ -29,4 +30,6 @@ export namespace tempo {
|
|
|
29
30
|
export const session = session_
|
|
30
31
|
/** One-shot settle: reads highest voucher from storage and submits on-chain. */
|
|
31
32
|
export const settle = settle_
|
|
33
|
+
/** Experimental websocket helpers for Tempo sessions. */
|
|
34
|
+
export const Ws = Ws_
|
|
32
35
|
}
|