mppx 0.5.6 → 0.5.8
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/Html.d.ts +2 -1
- package/dist/Html.d.ts.map +1 -1
- package/dist/Html.js +1 -1
- package/dist/Html.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/server/internal/html/config.d.ts +4 -0
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/stripe/server/Charge.d.ts +2 -4
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.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 +5 -5
- 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 +1 -1
- package/src/Challenge.test.ts +1 -1
- package/src/Challenge.ts +28 -9
- package/src/Html.ts +11 -1
- 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 +179 -89
- package/src/server/Transport.test.ts +31 -0
- package/src/server/Transport.ts +31 -2
- package/src/server/internal/html/config.ts +5 -0
- package/src/stripe/server/Charge.ts +2 -4
- 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 +119 -100
- 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
|
|
|
@@ -346,13 +368,7 @@ export declare namespace charge {
|
|
|
346
368
|
|
|
347
369
|
type Parameters = {
|
|
348
370
|
/** Render payment page when Accept header is text/html (e.g. in browsers) */
|
|
349
|
-
html?:
|
|
350
|
-
| boolean
|
|
351
|
-
| {
|
|
352
|
-
text?: Html.Text
|
|
353
|
-
theme?: Html.Theme
|
|
354
|
-
}
|
|
355
|
-
| undefined
|
|
371
|
+
html?: boolean | Html.Config | undefined
|
|
356
372
|
/** Testnet mode. */
|
|
357
373
|
testnet?: boolean | undefined
|
|
358
374
|
/**
|
|
@@ -363,10 +379,13 @@ export declare namespace charge {
|
|
|
363
379
|
* is explicitly provided; otherwise proofs remain reusable until the
|
|
364
380
|
* challenge expires.
|
|
365
381
|
*
|
|
382
|
+
* Replay protection requires a {@link Store.AtomicStore} so replay markers
|
|
383
|
+
* can be written atomically.
|
|
384
|
+
*
|
|
366
385
|
* Use a shared store in multi-instance deployments so consumed hashes and
|
|
367
386
|
* proofs are visible across all server instances.
|
|
368
387
|
*/
|
|
369
|
-
store?: Store.
|
|
388
|
+
store?: Store.AtomicStore | undefined
|
|
370
389
|
/**
|
|
371
390
|
* Whether to wait for the charge transaction to confirm on-chain before
|
|
372
391
|
* responding. @default true
|
|
@@ -603,40 +622,33 @@ function getProofStoreKey(challengeId: string): `mppx:charge:${string}` {
|
|
|
603
622
|
return `mppx:charge:proof:${challengeId}`
|
|
604
623
|
}
|
|
605
624
|
|
|
606
|
-
/** @internal */
|
|
607
|
-
async function assertHashUnused(
|
|
608
|
-
store: Store.Store<charge.StoreItemMap>,
|
|
609
|
-
hash: `0x${string}`,
|
|
610
|
-
): Promise<void> {
|
|
611
|
-
const seen = await store.get(getHashStoreKey(hash))
|
|
612
|
-
if (seen !== null)
|
|
613
|
-
throw new VerificationFailedError({ reason: 'Transaction hash has already been used' })
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/** @internal */
|
|
617
625
|
async function markHashUsed(
|
|
618
|
-
store: Store.
|
|
626
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
619
627
|
hash: `0x${string}`,
|
|
620
|
-
): Promise<
|
|
621
|
-
|
|
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
|
+
})
|
|
622
633
|
}
|
|
623
634
|
|
|
624
635
|
/** @internal */
|
|
625
|
-
async function
|
|
626
|
-
store: Store.
|
|
627
|
-
|
|
636
|
+
async function releaseHashUse(
|
|
637
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
638
|
+
hash: `0x${string}`,
|
|
628
639
|
): Promise<void> {
|
|
629
|
-
|
|
630
|
-
if (seen !== null)
|
|
631
|
-
throw new VerificationFailedError({ reason: 'Proof credential has already been used' })
|
|
640
|
+
await store.delete(getHashStoreKey(hash))
|
|
632
641
|
}
|
|
633
642
|
|
|
634
643
|
/** @internal */
|
|
635
644
|
async function markProofUsed(
|
|
636
|
-
store: Store.
|
|
645
|
+
store: Store.AtomicStore<charge.StoreItemMap>,
|
|
637
646
|
challengeId: string,
|
|
638
|
-
): Promise<
|
|
639
|
-
|
|
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
|
+
})
|
|
640
652
|
}
|
|
641
653
|
|
|
642
654
|
/** @internal */
|
|
@@ -682,6 +694,13 @@ class MismatchError extends PaymentError {
|
|
|
682
694
|
readonly type = 'https://paymentauth.org/problems/verification-failed'
|
|
683
695
|
|
|
684
696
|
constructor(reason: string, details: Record<string, string>) {
|
|
685
|
-
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
|
+
)
|
|
686
705
|
}
|
|
687
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
|
}
|