mppx 0.12.0 → 0.13.2
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 +64 -0
- package/dist/Method.d.ts +7 -1
- package/dist/Method.js +7 -2
- package/dist/Receipt.d.ts +2 -0
- package/dist/Receipt.js +2 -0
- package/dist/cli/cli.d.ts +8 -0
- package/dist/cli/cli.js +134 -136
- package/dist/cli/plugins/index.js +12 -14
- package/dist/cli/plugins/tempo.js +9 -2
- package/dist/cli/sessions/request.d.ts +1 -1
- package/dist/cli/sessions/request.js +6 -8
- package/dist/cli/utils.d.ts +11 -1
- package/dist/cli/utils.js +15 -2
- package/dist/cli/validate/challenge.d.ts +2 -0
- package/dist/cli/validate/challenge.js +19 -4
- package/dist/cli/validate/discovery.d.ts +1 -0
- package/dist/cli/validate/discovery.js +10 -1
- package/dist/cli/validate/payment.js +34 -12
- package/dist/client/Methods.d.ts +0 -2
- package/dist/client/Methods.js +0 -2
- package/dist/client/Mppx.js +1 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/internal/Fetch.d.ts +2 -0
- package/dist/client/internal/Fetch.js +22 -7
- package/dist/client/internal/MethodChallenge.d.ts +1 -0
- package/dist/evm/Types.js +1 -1
- package/dist/evm/server/Charge.js +5 -0
- package/dist/integrations/x402/Evm.js +4 -3
- package/dist/internal/HeaderCodec.d.ts +2 -2
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.js +4 -7
- package/dist/middlewares/elysia.d.ts +11 -8
- package/dist/middlewares/elysia.js +67 -14
- package/dist/middlewares/express.js +13 -6
- package/dist/middlewares/hono.js +4 -4
- package/dist/middlewares/nextjs.js +9 -5
- package/dist/proxy/internal/Headers.js +65 -0
- package/dist/proxy/services/stripe.js +1 -0
- package/dist/server/Mppx.d.ts +14 -2
- package/dist/server/Mppx.js +85 -44
- package/dist/server/NodeListener.js +71 -6
- package/dist/server/Request.d.ts +11 -1
- package/dist/server/Request.js +132 -13
- package/dist/server/Transport.d.ts +14 -1
- package/dist/server/Transport.js +1 -0
- package/dist/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +17 -2
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/tempo/Methods.d.ts +1 -0
- package/dist/tempo/Methods.js +1 -0
- package/dist/tempo/client/Charge.js +17 -13
- package/dist/tempo/client/Methods.d.ts +1 -5
- package/dist/tempo/client/Methods.js +10 -5
- package/dist/tempo/client/ResolveAccount.d.ts +3 -0
- package/dist/tempo/client/index.d.ts +0 -3
- package/dist/tempo/client/index.js +0 -2
- package/dist/tempo/internal/fee-payer.d.ts +6 -3
- package/dist/tempo/internal/fee-payer.js +23 -6
- package/dist/tempo/internal/machine-token-charge.d.ts +23 -0
- package/dist/tempo/internal/machine-token-charge.js +2 -1
- package/dist/tempo/legacy/index.d.ts +1 -3
- package/dist/tempo/legacy/index.js +1 -3
- package/dist/tempo/server/Charge.js +87 -53
- package/dist/tempo/server/Methods.d.ts +55 -10
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.js +28 -11
- package/dist/tempo/session/client/ChannelOps.js +5 -0
- package/dist/tempo/session/client/Session.d.ts +1 -0
- package/dist/tempo/session/client/Session.js +7 -4
- package/dist/tempo/session/client/SessionManager.js +3 -0
- package/dist/tempo/session/client/Transports.d.ts +8 -1
- package/dist/tempo/session/client/Transports.js +12 -3
- package/dist/tempo/session/precompile/Chain.js +16 -1
- package/dist/tempo/session/precompile/Protocol.d.ts +2 -0
- package/dist/tempo/session/precompile/Voucher.d.ts +16 -0
- package/dist/tempo/session/precompile/Voucher.js +48 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +20 -2
- package/dist/tempo/session/server/ChannelStore.js +25 -4
- package/dist/tempo/session/server/CredentialVerification.d.ts +4 -1
- package/dist/tempo/session/server/CredentialVerification.js +34 -6
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +2 -1
- package/dist/tempo/session/server/Settlement.d.ts +21 -0
- package/dist/tempo/session/server/Settlement.js +92 -17
- package/dist/tempo/session/server/Sse.d.ts +3 -1
- package/dist/tempo/session/server/Sse.js +105 -32
- package/dist/tempo/session/server/Transports.d.ts +24 -19
- package/dist/tempo/session/server/Transports.js +169 -58
- package/dist/tempo/session/server/Ws.d.ts +5 -1
- package/dist/tempo/session/server/Ws.js +75 -20
- package/dist/tempo/subscription/Store.d.ts +2 -0
- package/dist/tempo/subscription/Store.js +3 -0
- package/dist/validation/core.js +2 -1
- package/dist/x402/server/EvmCharge.d.ts +3 -1
- package/dist/x402/server/EvmCharge.js +14 -1
- package/package.json +6 -6
- package/dist/tempo/legacy/client/ChannelOps.d.ts +0 -69
- package/dist/tempo/legacy/client/ChannelOps.js +0 -168
- package/dist/tempo/legacy/client/Session.d.ts +0 -185
- package/dist/tempo/legacy/client/Session.js +0 -279
- package/dist/tempo/legacy/client/SessionManager.d.ts +0 -100
- package/dist/tempo/legacy/client/SessionManager.js +0 -706
- package/dist/tempo/legacy/client/index.d.ts +0 -7
- package/dist/tempo/legacy/client/index.js +0 -5
|
@@ -12,7 +12,7 @@ import * as Constants from '../../Constants.js';
|
|
|
12
12
|
import * as Credential from '../../Credential.js';
|
|
13
13
|
import { tempo as tempoMethods } from '../../tempo/client/index.js';
|
|
14
14
|
import { tip20ChannelEscrow } from '../../tempo/session/precompile/Protocol.js';
|
|
15
|
-
import { signVoucher } from '../../tempo/session/precompile/Voucher.js';
|
|
15
|
+
import { signCloseAuthorization, signVoucher } from '../../tempo/session/precompile/Voucher.js';
|
|
16
16
|
import { createDefaultStore, createKeychain, resolveAccountName } from '../account.js';
|
|
17
17
|
import { fetchTokenInfo, fmtBalance, isTempoAccount, isTestnet, link, pc, resolveChain, resolveRpcUrl, } from '../utils.js';
|
|
18
18
|
import { createPlugin } from './plugin.js';
|
|
@@ -227,14 +227,21 @@ export function tempo() {
|
|
|
227
227
|
const descriptor = _session?.descriptor;
|
|
228
228
|
if (!descriptor)
|
|
229
229
|
throw new Error('session descriptor not available');
|
|
230
|
+
const voucher = { channelId: channelId, cumulativeAmount };
|
|
231
|
+
const signature = await signVoucher(client, account, voucher, escrowContract, chainId);
|
|
230
232
|
return Credential.serialize({
|
|
231
233
|
challenge,
|
|
232
234
|
payload: {
|
|
233
235
|
action,
|
|
234
236
|
channelId,
|
|
237
|
+
...(action === 'close'
|
|
238
|
+
? {
|
|
239
|
+
closeSignature: await signCloseAuthorization(client, account, voucher, escrowContract, chainId),
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
235
242
|
descriptor,
|
|
236
243
|
cumulativeAmount: cumulativeAmount.toString(),
|
|
237
|
-
signature
|
|
244
|
+
signature,
|
|
238
245
|
},
|
|
239
246
|
source: `did:pkh:eip155:${chainId}:${account.address}`,
|
|
240
247
|
});
|
|
@@ -32,7 +32,7 @@ export declare function resolveSessionSelection(session: string, channelAlias: s
|
|
|
32
32
|
/** @internal Resolves whether the CLI accepts a server-advertised custom escrow. */
|
|
33
33
|
export declare function resolveAllowCustomEscrow(methodOptions: Record<string, string>): boolean | undefined;
|
|
34
34
|
/** @internal Resolves the manager deposit cap in human-readable token units. */
|
|
35
|
-
export declare function resolveSessionMaxDeposit(challenge: Challenge.Challenge, methodOptions: Record<string, string>, testnet: boolean): string | undefined;
|
|
35
|
+
export declare function resolveSessionMaxDeposit(challenge: Challenge.Challenge, methodOptions: Record<string, string>, testnet: boolean, decimals: number): string | undefined;
|
|
36
36
|
/** Runs one manager-backed request while holding the payer and payment-scope lock. */
|
|
37
37
|
export declare function runPersistentSessionRequest(parameters: PersistentSessionRequestParameters): Promise<void>;
|
|
38
38
|
//# sourceMappingURL=request.d.ts.map
|
|
@@ -5,7 +5,7 @@ import { getSessionManagerInternals } from '../../tempo/session/client/internal/
|
|
|
5
5
|
import { sessionManager } from '../../tempo/session/client/SessionManager.js';
|
|
6
6
|
import { isEventStream } from '../../tempo/session/precompile/Protocol.js';
|
|
7
7
|
import { resolvePersistentAccount } from '../account.js';
|
|
8
|
-
import { isTestnet, printResponseHeaders, resolveChain, resolveRpcUrl, } from '../utils.js';
|
|
8
|
+
import { fetchTokenInfo, isTestnet, printResponseHeaders, resolveChain, resolveRpcUrl, } from '../utils.js';
|
|
9
9
|
import { createSessionRegistry, SessionBusyError, sessionResourceUrl, sessionScope, sessionScopeKey, toChannelStore, } from './store.js';
|
|
10
10
|
/** Resolves `--session` and the `-M channel=` compatibility alias. */
|
|
11
11
|
export function resolveSessionSelection(session, channelAlias) {
|
|
@@ -26,9 +26,6 @@ export function resolveSessionSelection(session, channelAlias) {
|
|
|
26
26
|
exitCode: 2,
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
-
function sessionDecimals(challenge) {
|
|
30
|
-
return typeof challenge.request.decimals === 'number' ? challenge.request.decimals : 6;
|
|
31
|
-
}
|
|
32
29
|
/** @internal Resolves whether the CLI accepts a server-advertised custom escrow. */
|
|
33
30
|
export function resolveAllowCustomEscrow(methodOptions) {
|
|
34
31
|
const value = methodOptions.allowCustomEscrow;
|
|
@@ -45,12 +42,12 @@ export function resolveAllowCustomEscrow(methodOptions) {
|
|
|
45
42
|
});
|
|
46
43
|
}
|
|
47
44
|
/** @internal Resolves the manager deposit cap in human-readable token units. */
|
|
48
|
-
export function resolveSessionMaxDeposit(challenge, methodOptions, testnet) {
|
|
45
|
+
export function resolveSessionMaxDeposit(challenge, methodOptions, testnet, decimals) {
|
|
49
46
|
if (methodOptions.deposit !== undefined)
|
|
50
47
|
return methodOptions.deposit;
|
|
51
48
|
const suggested = challenge.request.suggestedDeposit;
|
|
52
49
|
if (typeof suggested === 'string')
|
|
53
|
-
return formatUnits(BigInt(suggested),
|
|
50
|
+
return formatUnits(BigInt(suggested), decimals);
|
|
54
51
|
return testnet ? '10' : undefined;
|
|
55
52
|
}
|
|
56
53
|
function validateReceipt(receipt, parameters) {
|
|
@@ -105,6 +102,7 @@ export async function runPersistentSessionRequest(parameters) {
|
|
|
105
102
|
message: `Challenge requires chainId ${challengeContext.chainId}, but RPC is chainId ${chain.id}.`,
|
|
106
103
|
exitCode: 2,
|
|
107
104
|
});
|
|
105
|
+
const { decimals } = await fetchTokenInfo(client, challengeContext.token, resolvedAccount.account.address, { requireDecimals: true });
|
|
108
106
|
const scope = {
|
|
109
107
|
payer: resolvedAccount.account.address,
|
|
110
108
|
payee: challengeContext.payee,
|
|
@@ -199,8 +197,8 @@ export async function runPersistentSessionRequest(parameters) {
|
|
|
199
197
|
client,
|
|
200
198
|
channelStore,
|
|
201
199
|
credentialContext: parameters.credentialContext,
|
|
202
|
-
decimals
|
|
203
|
-
maxDeposit: resolveSessionMaxDeposit(parameters.challenge, parameters.methodOptions, isTestnet(chain)),
|
|
200
|
+
decimals,
|
|
201
|
+
maxDeposit: resolveSessionMaxDeposit(parameters.challenge, parameters.methodOptions, isTestnet(chain), decimals),
|
|
204
202
|
fetch: async (input, init) => {
|
|
205
203
|
if (!replayPending)
|
|
206
204
|
return parameters.fetch(input, init);
|
package/dist/cli/utils.d.ts
CHANGED
|
@@ -96,7 +96,17 @@ export declare const usdc: Address;
|
|
|
96
96
|
export declare const mainnetTokens: readonly [`0x${string}`, `0x${string}`];
|
|
97
97
|
export declare const testnetTokens: readonly ['0x20c0000000000000000000000000000000000000', '0x20c0000000000000000000000000000000000001', '0x20c0000000000000000000000000000000000002', '0x20c0000000000000000000000000000000000003'];
|
|
98
98
|
export declare function isTestnet(chain: Chain): boolean;
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Fetches a Tempo token balance and display metadata.
|
|
101
|
+
*
|
|
102
|
+
* By default, unavailable decimals fall back to six for display compatibility.
|
|
103
|
+
* Set `requireDecimals` to throw instead when trusted on-chain decimals cannot
|
|
104
|
+
* be resolved.
|
|
105
|
+
*/
|
|
106
|
+
export declare function fetchTokenInfo(client: ReturnType<typeof createClient>, token: Address, account: Address, options?: {
|
|
107
|
+
/** Throws instead of using six decimals when token metadata is unavailable. */
|
|
108
|
+
requireDecimals?: boolean;
|
|
109
|
+
}): Promise<{
|
|
100
110
|
balance: bigint;
|
|
101
111
|
decimals: number;
|
|
102
112
|
symbol: string;
|
package/dist/cli/utils.js
CHANGED
|
@@ -259,14 +259,25 @@ function toBaseUnitAmount(value) {
|
|
|
259
259
|
export function isTestnet(chain) {
|
|
260
260
|
return chain.id !== tempoMainnet.id;
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Fetches a Tempo token balance and display metadata.
|
|
264
|
+
*
|
|
265
|
+
* By default, unavailable decimals fall back to six for display compatibility.
|
|
266
|
+
* Set `requireDecimals` to throw instead when trusted on-chain decimals cannot
|
|
267
|
+
* be resolved.
|
|
268
|
+
*/
|
|
269
|
+
export async function fetchTokenInfo(client, token, account, options = {}) {
|
|
263
270
|
const { Actions } = await import('viem/tempo');
|
|
264
271
|
const [balance, metadata] = await Promise.all([
|
|
265
272
|
Actions.token
|
|
266
273
|
.getBalance(client, { account, token })
|
|
267
274
|
.then(toBaseUnitAmount)
|
|
268
275
|
.catch(() => 0n),
|
|
269
|
-
Actions.token.getMetadata(client, { token }).catch(() =>
|
|
276
|
+
Actions.token.getMetadata(client, { token }).catch((error) => {
|
|
277
|
+
if (options.requireDecimals)
|
|
278
|
+
throw error;
|
|
279
|
+
return { symbol: token };
|
|
280
|
+
}),
|
|
270
281
|
]);
|
|
271
282
|
const knownSymbols = {
|
|
272
283
|
[pathUsd]: 'PathUSD',
|
|
@@ -274,6 +285,8 @@ export async function fetchTokenInfo(client, token, account) {
|
|
|
274
285
|
};
|
|
275
286
|
const symbol = knownSymbols[token] ?? metadata.symbol;
|
|
276
287
|
const decimals = 'decimals' in metadata && typeof metadata.decimals === 'number' ? metadata.decimals : 6;
|
|
288
|
+
if (options.requireDecimals && !('decimals' in metadata && typeof metadata.decimals === 'number'))
|
|
289
|
+
throw new Error('Token decimals are unavailable.');
|
|
277
290
|
return { balance, symbol, decimals, token };
|
|
278
291
|
}
|
|
279
292
|
export async function fetchBalanceLines(address, opts) {
|
|
@@ -11,6 +11,8 @@ export declare function validateChallenge(baseUrl: string, endpoint: EndpointSpe
|
|
|
11
11
|
resolvedBody?: string | undefined;
|
|
12
12
|
challenges?: Challenge.Challenge[] | undefined;
|
|
13
13
|
}>;
|
|
14
|
+
/** Validates that challenge realms cover the server hostname. */
|
|
15
|
+
export declare function validateRealmMatchesHost(challenges: Challenge.Challenge[], serverHost: string, results: CheckResult[]): void;
|
|
14
16
|
export declare function validateErrorHandling(baseUrl: string, endpoint: EndpointSpec, options?: {
|
|
15
17
|
body?: string | undefined;
|
|
16
18
|
query?: string[] | undefined;
|
|
@@ -15,7 +15,14 @@ function detectTestnet(challenge) {
|
|
|
15
15
|
}
|
|
16
16
|
export async function validateChallenge(baseUrl, endpoint, verbose, options) {
|
|
17
17
|
const results = [];
|
|
18
|
-
|
|
18
|
+
let url;
|
|
19
|
+
try {
|
|
20
|
+
url = buildUrl(baseUrl, endpoint, options?.query);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
results.push(fail('Request URL valid', error.message));
|
|
24
|
+
return { results };
|
|
25
|
+
}
|
|
19
26
|
const fetchHeaders = parseHeaders(options?.extraHeaders);
|
|
20
27
|
let fetchBody;
|
|
21
28
|
// If the user explicitly provided a body, include it on the first request
|
|
@@ -167,11 +174,19 @@ function validateExpiration(challenges, results) {
|
|
|
167
174
|
const soonest = Math.min(...challenges.map((ch) => new Date(ch.expires).getTime() - now.getTime()));
|
|
168
175
|
results.push(check('Challenge expires in the future', `${Math.round(soonest / 60000)}m from now`));
|
|
169
176
|
}
|
|
170
|
-
|
|
177
|
+
/** Validates that challenge realms cover the server hostname. */
|
|
178
|
+
export function validateRealmMatchesHost(challenges, serverHost, results) {
|
|
179
|
+
const normalizeHostname = (value) => value.toLowerCase().replace(/\.$/, '');
|
|
180
|
+
const normalizedServerHost = normalizeHostname(serverHost);
|
|
171
181
|
const realms = [...new Set(challenges.map((ch) => ch.realm ?? ''))];
|
|
172
|
-
const badRealms = realms.filter((
|
|
182
|
+
const badRealms = realms.filter((realm) => {
|
|
183
|
+
const normalizedRealm = normalizeHostname(realm);
|
|
184
|
+
return (!normalizedRealm ||
|
|
185
|
+
(normalizedRealm !== normalizedServerHost &&
|
|
186
|
+
!normalizedServerHost.endsWith(`.${normalizedRealm}`)));
|
|
187
|
+
});
|
|
173
188
|
if (badRealms.length > 0) {
|
|
174
|
-
results.push(
|
|
189
|
+
results.push(fail('Realm matches server hostname', `realm="${badRealms[0]}" vs host="${serverHost}"`, 'Set the realm to your production hostname (or base domain) in the challenge.'));
|
|
175
190
|
}
|
|
176
191
|
else {
|
|
177
192
|
results.push(check('Realm matches server hostname'));
|
|
@@ -9,5 +9,6 @@ export declare function fetchDiscoveryDoc(baseUrl: string): Promise<{
|
|
|
9
9
|
export declare function validateLlmsDoc(baseUrl: string): Promise<CheckResult>;
|
|
10
10
|
export declare function extractEndpointsFromDiscovery(doc: Record<string, unknown>): EndpointSpec[];
|
|
11
11
|
export declare function extractRequestBodyFromDiscovery(doc: Record<string, unknown>, endpoint: EndpointSpec): string | undefined;
|
|
12
|
+
/** Builds an endpoint URL and rejects paths that could escape the discovery document's origin. */
|
|
12
13
|
export declare function buildUrl(baseUrl: string, endpoint: EndpointSpec, query?: string[]): string;
|
|
13
14
|
//# sourceMappingURL=discovery.d.ts.map
|
|
@@ -117,14 +117,23 @@ export function extractRequestBodyFromDiscovery(doc, endpoint) {
|
|
|
117
117
|
}
|
|
118
118
|
return undefined;
|
|
119
119
|
}
|
|
120
|
+
/** Builds an endpoint URL and rejects paths that could escape the discovery document's origin. */
|
|
120
121
|
export function buildUrl(baseUrl, endpoint, query) {
|
|
122
|
+
if (!endpoint.path.startsWith('/') ||
|
|
123
|
+
endpoint.path.startsWith('//') ||
|
|
124
|
+
endpoint.path.includes('\\'))
|
|
125
|
+
throw new Error(`OpenAPI path must be an origin-relative path: ${endpoint.path}`);
|
|
121
126
|
let path = endpoint.path;
|
|
122
127
|
if (endpoint.parameters) {
|
|
123
128
|
path = substitutePathParams(path, endpoint.parameters);
|
|
124
129
|
}
|
|
125
130
|
// Strip leading slash so URL resolves relative to baseUrl's path, not root.
|
|
126
131
|
const relativePath = path.startsWith('/') ? path.slice(1) : path;
|
|
127
|
-
|
|
132
|
+
const base = new URL(baseUrl.replace(/\/?$/, '/'));
|
|
133
|
+
const resolved = new URL(relativePath, base);
|
|
134
|
+
if (resolved.origin !== base.origin)
|
|
135
|
+
throw new Error(`OpenAPI path must be an origin-relative path: ${endpoint.path}`);
|
|
136
|
+
let url = resolved.href;
|
|
128
137
|
if (query) {
|
|
129
138
|
const u = new URL(url);
|
|
130
139
|
for (const q of query) {
|
|
@@ -15,6 +15,7 @@ import { isTempoSessionChallenge } from '../../tempo/session/client/Transports.j
|
|
|
15
15
|
import { resolveAccount, resolveAccountName } from '../account.js';
|
|
16
16
|
import { assertSamePaymentRequest, flattenConfigMethods, loadConfig, preparePayment, resolvePlugin, } from '../internal.js';
|
|
17
17
|
import { fetchTokenInfo, confirm, pc } from '../utils.js';
|
|
18
|
+
import { validateRealmMatchesHost } from './challenge.js';
|
|
18
19
|
import { buildUrl } from './discovery.js';
|
|
19
20
|
import { check, fail, fetchWithTimeout, formatBytes, isValidIntegerAmount, parseHeaders, skip, warn, } from './helpers.js';
|
|
20
21
|
async function provisionAndPayTestnet(challenge, verbose, silent) {
|
|
@@ -95,16 +96,19 @@ function resolveStripeKey(verbose) {
|
|
|
95
96
|
}
|
|
96
97
|
async function fetchEvmTokenInfo(chain, token, account) {
|
|
97
98
|
const client = createClient({ chain, transport: http() });
|
|
98
|
-
const [balance, symbol] = await Promise.all([
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
99
|
+
const [balance, decimals, symbol] = await Promise.all([
|
|
100
|
+
account
|
|
101
|
+
? readContract(client, {
|
|
102
|
+
address: token,
|
|
103
|
+
abi: erc20Abi,
|
|
104
|
+
functionName: 'balanceOf',
|
|
105
|
+
args: [account],
|
|
106
|
+
})
|
|
107
|
+
: 0n,
|
|
108
|
+
readContract(client, { address: token, abi: erc20Abi, functionName: 'decimals' }),
|
|
105
109
|
readContract(client, { address: token, abi: erc20Abi, functionName: 'symbol' }).catch(() => undefined),
|
|
106
110
|
]);
|
|
107
|
-
return { balance, symbol: symbol ?? undefined };
|
|
111
|
+
return { balance, decimals, symbol: symbol ?? undefined };
|
|
108
112
|
}
|
|
109
113
|
function resolveEvmChain(chainId) {
|
|
110
114
|
const all = Object.values(viemChains);
|
|
@@ -162,6 +166,14 @@ export async function validatePaymentFlow(baseUrl, endpoint, verbose, options) {
|
|
|
162
166
|
results.push(fail('Payment: parse challenge', 'No Payment challenges in response'));
|
|
163
167
|
return results;
|
|
164
168
|
}
|
|
169
|
+
const realmResults = [];
|
|
170
|
+
validateRealmMatchesHost(challenges, new URL(url).hostname, realmResults);
|
|
171
|
+
const realmFailures = realmResults.filter((result) => result.severity === 'fail');
|
|
172
|
+
if (realmFailures.length > 0) {
|
|
173
|
+
results.push(...realmFailures);
|
|
174
|
+
options.onResults?.(realmFailures);
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
165
177
|
const loaded = await loadConfig().catch(() => undefined);
|
|
166
178
|
// Testnet Tempo: always use ephemeral wallet (zero-setup, free money)
|
|
167
179
|
const tempoTestnetChallenge = challenges.find((ch) => {
|
|
@@ -274,7 +286,7 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
274
286
|
const requiredAmount = isValidIntegerAmount(request.amount)
|
|
275
287
|
? BigInt(request.amount)
|
|
276
288
|
: undefined;
|
|
277
|
-
|
|
289
|
+
let decimals = methodDetails?.decimals ?? request.decimals ?? 6;
|
|
278
290
|
const currency = request.currency;
|
|
279
291
|
const { plugin, method: directMethod } = resolvePlugin(challenge, loaded?.config);
|
|
280
292
|
if (!plugin && !directMethod) {
|
|
@@ -306,22 +318,30 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
306
318
|
if (chainId)
|
|
307
319
|
paymentChain = resolveEvmChain(chainId);
|
|
308
320
|
}
|
|
321
|
+
if (requiredAmount && currency && !paymentChain) {
|
|
322
|
+
results.push(skip(tag, 'unable to resolve payment chain and verify token decimals'));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
309
325
|
let tokenSymbol;
|
|
310
|
-
if (
|
|
326
|
+
if (requiredAmount && currency && paymentChain) {
|
|
311
327
|
try {
|
|
312
328
|
let balance;
|
|
313
329
|
if (challenge.method === Constants.Methods.tempo) {
|
|
314
330
|
const client = createClient({ chain: paymentChain, transport: http() });
|
|
315
|
-
const info = await fetchTokenInfo(client, currency, walletAddress)
|
|
331
|
+
const info = await fetchTokenInfo(client, currency, (walletAddress ?? zeroAddress), {
|
|
332
|
+
requireDecimals: true,
|
|
333
|
+
});
|
|
316
334
|
balance = info.balance;
|
|
335
|
+
decimals = info.decimals;
|
|
317
336
|
tokenSymbol = info.symbol;
|
|
318
337
|
}
|
|
319
338
|
else {
|
|
320
339
|
const info = await fetchEvmTokenInfo(paymentChain, currency, walletAddress);
|
|
321
340
|
balance = info.balance;
|
|
341
|
+
decimals = info.decimals;
|
|
322
342
|
tokenSymbol = info.symbol;
|
|
323
343
|
}
|
|
324
|
-
if (balance < requiredAmount) {
|
|
344
|
+
if (walletAddress && balance < requiredAmount) {
|
|
325
345
|
const requiredDisplay = formatAmount(requiredAmount, decimals);
|
|
326
346
|
const balanceDisplay = formatAmount(balance, decimals);
|
|
327
347
|
const symbol = tokenSymbol ?? 'tokens';
|
|
@@ -332,6 +352,8 @@ async function attemptCryptoPayment(challenge, tag, ctx) {
|
|
|
332
352
|
catch (e) {
|
|
333
353
|
if (verbose)
|
|
334
354
|
console.log(pc.dim(` Balance check skipped: ${e.message}`));
|
|
355
|
+
results.push(skip(tag, 'unable to verify token balance and decimals from the payment chain'));
|
|
356
|
+
return;
|
|
335
357
|
}
|
|
336
358
|
}
|
|
337
359
|
// Prompt
|
package/dist/client/Methods.d.ts
CHANGED
|
@@ -4,6 +4,4 @@ export { subscription } from '../tempo/client/Subscription.js';
|
|
|
4
4
|
export { session, tempo } from '../tempo/client/index.js';
|
|
5
5
|
export { session as sessionMethod } from '../tempo/session/client/Session.js';
|
|
6
6
|
export { sessionManager } from '../tempo/session/client/SessionManager.js';
|
|
7
|
-
export { session as sessionLegacy } from '../tempo/legacy/client/index.js';
|
|
8
|
-
export { sessionManager as sessionLegacyManager } from '../tempo/legacy/client/index.js';
|
|
9
7
|
//# sourceMappingURL=Methods.d.ts.map
|
package/dist/client/Methods.js
CHANGED
|
@@ -4,6 +4,4 @@ export { subscription } from '../tempo/client/Subscription.js';
|
|
|
4
4
|
export { session, tempo } from '../tempo/client/index.js';
|
|
5
5
|
export { session as sessionMethod } from '../tempo/session/client/Session.js';
|
|
6
6
|
export { sessionManager } from '../tempo/session/client/SessionManager.js';
|
|
7
|
-
export { session as sessionLegacy } from '../tempo/legacy/client/index.js';
|
|
8
|
-
export { sessionManager as sessionLegacyManager } from '../tempo/legacy/client/index.js';
|
|
9
7
|
//# sourceMappingURL=Methods.js.map
|
package/dist/client/Mppx.js
CHANGED
package/dist/client/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as Constants from '../Constants.js';
|
|
2
2
|
export * as Expires from '../Expires.js';
|
|
3
3
|
export * as Fetch from './internal/Fetch.js';
|
|
4
|
-
export { evm, session, sessionManager, sessionMethod,
|
|
4
|
+
export { evm, session, sessionManager, sessionMethod, stripe, tempo } from './Methods.js';
|
|
5
5
|
export { createChannelStore, createJsonChannelStore, entryKey, type ChannelStore, type JsonChannelKv, } from '../tempo/session/client/ChannelStore.js';
|
|
6
6
|
export type { ChargeContext } from '../tempo/client/Charge.js';
|
|
7
7
|
export type { ResolveAccount, ResolveAccountCall, ResolveAccountInfo, ResolveAccountOperation, } from '../tempo/client/ResolveAccount.js';
|
package/dist/client/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * as Constants from '../Constants.js';
|
|
2
2
|
export * as Expires from '../Expires.js';
|
|
3
3
|
export * as Fetch from './internal/Fetch.js';
|
|
4
|
-
export { evm, session, sessionManager, sessionMethod,
|
|
4
|
+
export { evm, session, sessionManager, sessionMethod, stripe, tempo } from './Methods.js';
|
|
5
5
|
export { createChannelStore, createJsonChannelStore, entryKey, } from '../tempo/session/client/ChannelStore.js';
|
|
6
6
|
export * as Mppx from './Mppx.js';
|
|
7
7
|
export * as Transport from './Transport.js';
|
|
@@ -171,6 +171,8 @@ export declare function createEventDispatcher<methods extends readonly Method.An
|
|
|
171
171
|
/** @internal */
|
|
172
172
|
export declare function unwrapFetch(fetch: typeof globalThis.fetch): typeof globalThis.fetch;
|
|
173
173
|
/** @internal */
|
|
174
|
+
export declare function resolveRequestSignal(input: RequestInfo | URL, init: RequestInit | undefined): AbortSignal | undefined;
|
|
175
|
+
/** @internal */
|
|
174
176
|
export declare function validateCredentialHeaderValue(credential: string): void;
|
|
175
177
|
/** @internal */
|
|
176
178
|
/** @internal */
|
|
@@ -122,6 +122,7 @@ export function from(config) {
|
|
|
122
122
|
context: credentialContext,
|
|
123
123
|
fetch: baseFetch,
|
|
124
124
|
input: paymentInput,
|
|
125
|
+
signal: resolveRequestSignal(input, init),
|
|
125
126
|
}),
|
|
126
127
|
};
|
|
127
128
|
const promise = attempt
|
|
@@ -165,15 +166,24 @@ export function from(config) {
|
|
|
165
166
|
method: selected.method,
|
|
166
167
|
response,
|
|
167
168
|
}));
|
|
168
|
-
response = await baseFetch(paymentInput,
|
|
169
|
-
...
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
response = await baseFetch(paymentInput, {
|
|
170
|
+
...transport.setCredential({
|
|
171
|
+
...fetchInit,
|
|
172
|
+
headers: initialRequest.headers,
|
|
173
|
+
}, credential, { challenge: selectedChallenge }),
|
|
174
|
+
// Runtimes do not strip custom payment headers on cross-origin redirects.
|
|
175
|
+
redirect: 'manual',
|
|
176
|
+
});
|
|
172
177
|
const paymentRequired = await transport.isPaymentRequired(response, transportRequest);
|
|
173
178
|
if (!paymentRequired) {
|
|
174
179
|
await settleAttempt(prepared, {
|
|
175
180
|
response,
|
|
176
|
-
|
|
181
|
+
// A redirect does not tell us whether the paid request was accepted.
|
|
182
|
+
status: response.status >= 300 && response.status < 400
|
|
183
|
+
? 'pending'
|
|
184
|
+
: response.ok
|
|
185
|
+
? 'accepted'
|
|
186
|
+
: 'rejected',
|
|
177
187
|
});
|
|
178
188
|
preparedCredential = undefined;
|
|
179
189
|
}
|
|
@@ -576,8 +586,13 @@ function cloneRequestInput(input) {
|
|
|
576
586
|
return input;
|
|
577
587
|
}
|
|
578
588
|
}
|
|
579
|
-
|
|
580
|
-
|
|
589
|
+
/** @internal */
|
|
590
|
+
export function resolveRequestSignal(input, init) {
|
|
591
|
+
if (init?.signal === null)
|
|
592
|
+
return undefined;
|
|
593
|
+
if (init?.signal !== undefined)
|
|
594
|
+
return init.signal;
|
|
595
|
+
return input instanceof Request ? input.signal : undefined;
|
|
581
596
|
}
|
|
582
597
|
/** @internal */
|
|
583
598
|
function isWrappedFetch(fetch) {
|
|
@@ -7,6 +7,7 @@ export type HandlerParameters = {
|
|
|
7
7
|
context?: unknown;
|
|
8
8
|
fetch: typeof globalThis.fetch;
|
|
9
9
|
input: RequestInfo | URL;
|
|
10
|
+
signal?: AbortSignal | undefined;
|
|
10
11
|
};
|
|
11
12
|
/** Internal client-method challenge hook. */
|
|
12
13
|
export type Handler = (parameters: HandlerParameters) => MaybePromise<void>;
|
package/dist/evm/Types.js
CHANGED
|
@@ -87,7 +87,7 @@ export function authorizationDomain(parameters) {
|
|
|
87
87
|
}
|
|
88
88
|
/** Computes the Payment-auth EVM challenge hash used as the EIP-3009 nonce. */
|
|
89
89
|
export function challengeHash(challenge) {
|
|
90
|
-
return Hash.keccak256(Bytes.fromString(
|
|
90
|
+
return Hash.keccak256(Bytes.fromString(JSON.stringify([challenge.id, challenge.realm])), {
|
|
91
91
|
as: 'Hex',
|
|
92
92
|
});
|
|
93
93
|
}
|
|
@@ -171,6 +171,7 @@ function createPaths(config) {
|
|
|
171
171
|
function createMppPath() {
|
|
172
172
|
const transport = ServerTransport.http();
|
|
173
173
|
return {
|
|
174
|
+
credentialHeaders: transport.credentialHeaders ?? [],
|
|
174
175
|
bindCredential: (options) => transport.bindCredential?.(options) ?? options.credential,
|
|
175
176
|
captureRequest: transport.captureRequest,
|
|
176
177
|
getCredential: transport.getCredential,
|
|
@@ -181,10 +182,14 @@ function createMppPath() {
|
|
|
181
182
|
function httpTransport(paths) {
|
|
182
183
|
return ServerTransport.from({
|
|
183
184
|
name: 'evm-http',
|
|
185
|
+
credentialHeaders: [
|
|
186
|
+
...new Set([...paths.mpp.credentialHeaders, ...paths.x402.credentialHeaders]),
|
|
187
|
+
],
|
|
184
188
|
captureRequest: paths.mpp.captureRequest,
|
|
185
189
|
getCredential(input) {
|
|
186
190
|
return paths.mpp.getCredential(input) ?? paths.x402.getCredential(input);
|
|
187
191
|
},
|
|
192
|
+
matchCredential: paths.x402.matchCredential,
|
|
188
193
|
bindCredential(options) {
|
|
189
194
|
if (X402.isPendingCredential(options.credential))
|
|
190
195
|
return paths.x402.bindCredential(options);
|
|
@@ -4,7 +4,6 @@ import { evm } from '../../evm/server/Methods.js';
|
|
|
4
4
|
import { eip3009 } from '../../evm/Types.js';
|
|
5
5
|
import * as Expires from '../../Expires.js';
|
|
6
6
|
import * as Mppx from '../../server/Mppx.js';
|
|
7
|
-
import { tempo } from '../../tempo/server/Methods.js';
|
|
8
7
|
import * as Types from '../../x402/Types.js';
|
|
9
8
|
/** Creates a native MPP handler backed by a compatible x402 EVM requirement. */
|
|
10
9
|
export function createHandler(parameters) {
|
|
@@ -37,8 +36,10 @@ export function createHandler(parameters) {
|
|
|
37
36
|
return { reference: result.reference };
|
|
38
37
|
},
|
|
39
38
|
});
|
|
39
|
+
// Only advertise a native rail whose credential can be verified and settled
|
|
40
|
+
// by the selected x402 scheme. A Tempo transaction is not an EIP-3009 payload.
|
|
40
41
|
const payment = Mppx.create({
|
|
41
|
-
methods: [
|
|
42
|
+
methods: [evmMethod],
|
|
42
43
|
...(parameters.config.realm ? { realm: parameters.config.realm } : {}),
|
|
43
44
|
secretKey: parameters.config.secretKey,
|
|
44
45
|
});
|
|
@@ -50,7 +51,7 @@ export function createHandler(parameters) {
|
|
|
50
51
|
expires: Expires.seconds(requirement.maxTimeoutSeconds),
|
|
51
52
|
scope: parameters.paymentRequired.resource.url,
|
|
52
53
|
};
|
|
53
|
-
const handler = payment.compose([payment.
|
|
54
|
+
const handler = payment.compose([payment.evm.charge, options]);
|
|
54
55
|
return async (request) => ({
|
|
55
56
|
payment: await handler(request),
|
|
56
57
|
skipHandlerResponse,
|
|
@@ -7,8 +7,8 @@ import type * as z from '../zod.js';
|
|
|
7
7
|
* can opt into the exact wire encoding their protocol expects.
|
|
8
8
|
*/
|
|
9
9
|
export declare function createJson<const schema extends z.ZodMiniType>(schema: schema): {
|
|
10
|
-
encode(value: z.
|
|
11
|
-
decode(value: string): z.
|
|
10
|
+
encode(value: z.output<schema>): string;
|
|
11
|
+
decode(value: string): z.output<schema>;
|
|
12
12
|
};
|
|
13
13
|
/** Error thrown when a JSON header value is not valid base64-encoded JSON. */
|
|
14
14
|
export declare class InvalidJsonHeaderError extends Error {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Current mppx package version. */
|
|
2
|
-
export declare const version = "0.
|
|
2
|
+
export declare const version = "0.13.2";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.13.2";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/internal/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.ts.
|
|
2
2
|
/** Current mppx package version. */
|
|
3
|
-
export const version = '0.
|
|
3
|
+
export const version = '0.13.2';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -88,14 +88,11 @@ export function isPaymentRequiredError(error) {
|
|
|
88
88
|
}
|
|
89
89
|
/** @internal */
|
|
90
90
|
async function createCredential(challenge, config) {
|
|
91
|
-
const { context,
|
|
92
|
-
const mi = methods.find((m) => m.name === challenge.method && m.intent === challenge.intent);
|
|
93
|
-
if (!mi)
|
|
94
|
-
throw new Error(`No method found for "${challenge.method}.${challenge.intent}". Available: ${methods.map((m) => `${m.name}.${m.intent}`).join(', ')}`);
|
|
91
|
+
const { context, method } = config;
|
|
95
92
|
if (challenge.expires)
|
|
96
93
|
Expires.assert(challenge.expires, challenge.id);
|
|
97
|
-
const parsedContext =
|
|
98
|
-
return
|
|
94
|
+
const parsedContext = method.context && context !== undefined ? method.context.parse(context) : undefined;
|
|
95
|
+
return method.createCredential(parsedContext !== undefined ? { challenge, context: parsedContext } : { challenge });
|
|
99
96
|
}
|
|
100
97
|
function createPaymentAwareCallTool(callTool, config) {
|
|
101
98
|
const methods = config.methods.flat();
|
|
@@ -121,7 +118,7 @@ function createPaymentAwareCallTool(callTool, config) {
|
|
|
121
118
|
}
|
|
122
119
|
const credential = await createCredential(selected.challenge, {
|
|
123
120
|
context: call.context,
|
|
124
|
-
|
|
121
|
+
method: selected.method,
|
|
125
122
|
});
|
|
126
123
|
const parsed = Credential.deserialize(credential);
|
|
127
124
|
try {
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import { Elysia, type
|
|
1
|
+
import { Elysia, type AfterHandler, type OptionalHandler } from 'elysia';
|
|
2
2
|
import { type GenerateConfig, type RouteConfig } from '../discovery/OpenApi.js';
|
|
3
3
|
import * as Mppx_core from '../server/Mppx.js';
|
|
4
4
|
import * as Mppx_internal from './internal/mppx.js';
|
|
5
5
|
export * from '../server/Methods.js';
|
|
6
|
-
type ElysiaHook =
|
|
6
|
+
type ElysiaHook = OptionalHandler & {
|
|
7
|
+
afterHandle: AfterHandler;
|
|
8
|
+
beforeHandle: OptionalHandler;
|
|
9
|
+
};
|
|
7
10
|
export declare namespace Mppx {
|
|
8
11
|
/**
|
|
9
12
|
* Creates an Elysia-aware payment handler where each intent
|
|
10
|
-
* returns
|
|
13
|
+
* returns Elysia lifecycle hooks.
|
|
11
14
|
*
|
|
12
|
-
* Use with `.guard()`
|
|
13
|
-
*
|
|
15
|
+
* Use with `.guard()` so both verification and response wrapping are scoped
|
|
16
|
+
* to the same routes.
|
|
14
17
|
*
|
|
15
18
|
* @example
|
|
16
19
|
* ```ts
|
|
@@ -21,7 +24,7 @@ export declare namespace Mppx {
|
|
|
21
24
|
*
|
|
22
25
|
* const app = new Elysia()
|
|
23
26
|
* .guard(
|
|
24
|
-
*
|
|
27
|
+
* mppx.charge({ amount: '1' }),
|
|
25
28
|
* (app) => app.get('/premium', () => ({ data: 'paid content' })),
|
|
26
29
|
* )
|
|
27
30
|
* ```
|
|
@@ -29,7 +32,7 @@ export declare namespace Mppx {
|
|
|
29
32
|
function create<const methods extends Mppx_core.Methods>(config: Mppx_core.create.Config<methods>): Mppx_internal.Wrap<Mppx_core.Mppx<methods>, ElysiaHook>;
|
|
30
33
|
}
|
|
31
34
|
/**
|
|
32
|
-
* Elysia
|
|
35
|
+
* Elysia lifecycle hooks that gate a route behind a payment intent.
|
|
33
36
|
*
|
|
34
37
|
* Returns a 402 challenge if no valid credential is provided.
|
|
35
38
|
*
|
|
@@ -43,7 +46,7 @@ export declare namespace Mppx {
|
|
|
43
46
|
*
|
|
44
47
|
* const app = new Elysia()
|
|
45
48
|
* .guard(
|
|
46
|
-
*
|
|
49
|
+
* payment(mppx.charge, { amount: '1' }),
|
|
47
50
|
* (app) => app.get('/premium', () => ({ data: 'paid content' })),
|
|
48
51
|
* )
|
|
49
52
|
* ```
|