mppx 0.17.1 → 0.17.4
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 +65 -0
- package/dist/Errors.d.ts +94 -0
- package/dist/Errors.js +83 -0
- package/dist/Store.d.ts +413 -0
- package/dist/Store.js +837 -0
- package/dist/cli/cli.js +141 -136
- package/dist/cli/internal.js +28 -6
- package/dist/cli/plugins/index.js +16 -11
- package/dist/cli/sessions/Manager.js +12 -1
- package/dist/cli/utils.d.ts +2 -0
- package/dist/cli/utils.js +6 -0
- package/dist/cli/validate/challenge.d.ts +4 -0
- package/dist/cli/validate/challenge.js +9 -6
- package/dist/client/Mppx.d.ts +1 -1
- package/dist/client/Mppx.js +7 -3
- package/dist/client/internal/Fetch.d.ts +1 -1
- package/dist/client/internal/Fetch.js +9 -0
- package/dist/client/internal/MethodResponse.d.ts +8 -0
- package/dist/client/internal/MethodResponse.js +9 -0
- package/dist/client/internal/ProofRealm.d.ts +3 -3
- package/dist/client/internal/ProofRealm.js +4 -4
- package/dist/client/internal/protocols/Mcp.d.ts +4 -0
- package/dist/client/internal/protocols/Mcp.js +27 -14
- package/dist/cloudflare/ChannelStore.d.ts +97 -0
- package/dist/cloudflare/ChannelStore.js +304 -0
- package/dist/cloudflare/Channels.d.ts +92 -0
- package/dist/cloudflare/Channels.js +325 -0
- package/dist/cloudflare/index.d.ts +5 -0
- package/dist/cloudflare/index.js +5 -0
- package/dist/identity/Client.js +7 -1
- package/dist/internal/sse.d.ts +21 -0
- package/dist/internal/sse.js +82 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/mcp/client/McpClient.d.ts +2 -2
- package/dist/mcp/client/McpClient.js +5 -2
- package/dist/proxy/Proxy.d.ts +2 -0
- package/dist/proxy/Proxy.js +31 -6
- package/dist/proxy/internal/Replay.js +1 -1
- package/dist/server/Mppx.js +5 -4
- package/dist/server/Transport.js +1 -1
- package/dist/server/internal/html/config.d.ts +7 -1
- package/dist/server/internal/html/config.js +4 -0
- package/dist/stripe/internal/types.d.ts +82 -6
- package/dist/stripe/server/Charge.js +1 -2
- package/dist/stripe/server/Methods.d.ts +2 -0
- package/dist/stripe/server/Methods.js +2 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/record-payment.d.ts +49 -0
- package/dist/stripe/server/internal/record-payment.js +75 -18
- package/dist/tempo/Methods.d.ts +8 -0
- package/dist/tempo/Methods.js +18 -2
- package/dist/tempo/client/Charge.d.ts +32 -1
- package/dist/tempo/client/Charge.js +201 -3
- package/dist/tempo/client/Methods.d.ts +16 -0
- package/dist/tempo/internal/fee-payer.d.ts +64 -14
- package/dist/tempo/internal/fee-payer.js +231 -20
- package/dist/tempo/internal/zone.d.ts +79 -0
- package/dist/tempo/internal/zone.js +133 -0
- package/dist/tempo/legacy/session/Chain.js +2 -0
- package/dist/tempo/server/Charge.d.ts +7 -3
- package/dist/tempo/server/Charge.js +100 -6
- package/dist/tempo/server/Methods.d.ts +40 -0
- package/dist/tempo/server/Subscription.js +1 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +6 -0
- package/dist/tempo/server/internal/transport.js +21 -21
- package/dist/tempo/session/client/ChannelStore.d.ts +2 -0
- package/dist/tempo/session/client/CredentialState.js +2 -0
- package/dist/tempo/session/client/Session.d.ts +11 -1
- package/dist/tempo/session/client/Session.js +39 -6
- package/dist/tempo/session/client/SessionManager.d.ts +1 -1
- package/dist/tempo/session/client/SessionManager.js +8 -4
- package/dist/tempo/session/client/Transports.d.ts +2 -0
- package/dist/tempo/session/client/Transports.js +4 -1
- package/dist/tempo/session/precompile/Chain.js +1 -0
- package/dist/tempo/session/server/ChannelStore.d.ts +14 -0
- package/dist/tempo/session/server/ChannelStore.js +10 -3
- package/dist/tempo/session/server/CredentialVerification.js +1 -1
- package/dist/tempo/session/server/MeteredStream.d.ts +4 -0
- package/dist/tempo/session/server/MeteredStream.js +6 -1
- package/dist/tempo/session/server/Sse.d.ts +59 -0
- package/dist/tempo/session/server/Sse.js +175 -10
- package/dist/validation/core.js +2 -0
- package/dist/zod.d.ts +2 -1
- package/dist/zod.js +1 -1
- package/package.json +5 -3
- package/dist/stripe/server/internal/html/types.d.ts +0 -2
- package/dist/stripe/server/internal/html/types.js +0 -2
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { zeroAddress } from 'viem';
|
|
2
|
+
import { ChannelNotFoundError, VerificationFailedError } from '../Errors.js';
|
|
3
|
+
import * as Store from '../Store.js';
|
|
4
|
+
import { parsePkhSource } from '../tempo/internal/proof.js';
|
|
5
|
+
import { tip20ChannelEscrow } from '../tempo/session/precompile/Protocol.js';
|
|
6
|
+
import { isPrecompileState } from '../tempo/session/server/ChannelStore.js';
|
|
7
|
+
import { settle as settleChannel, SettlementCheckpointError, } from '../tempo/session/server/Settlement.js';
|
|
8
|
+
import { isChannelState, pendingClose, unsettledAmount } from './ChannelStore.js';
|
|
9
|
+
/**
|
|
10
|
+
* Tempo session channel storage on a {@link ChannelStore} Durable Object namespace, to spread
|
|
11
|
+
* into `tempo.session()`: `store` keeps each channel in its own object, which schedules its
|
|
12
|
+
* settlement, and `resolveChannelId` finds the payer's open channel to the same payee, token,
|
|
13
|
+
* escrow, operator and chain, so a client that lost its state resumes it instead of opening another.
|
|
14
|
+
* The lookup needs the payer's identity, which only a `bootstrap` HEAD proof supplies: enable
|
|
15
|
+
* `bootstrap` on the session and in the client's session manager, or it never resolves.
|
|
16
|
+
*
|
|
17
|
+
* The store keeps that lookup current: after a write opens, closes or finalizes a channel,
|
|
18
|
+
* it adds or removes the channel under its payer and authorized signer. Each lookup entry is
|
|
19
|
+
* its own key holding the payer's open channels there, most recent first, so closing one leaves
|
|
20
|
+
* the others findable. The lookup returns the first whose channel still resumes, so a delayed
|
|
21
|
+
* write for a channel that has since closed cannot hide an open one, and mppx validates any
|
|
22
|
+
* channel it resolves before reusing it.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { channels } from 'mppx/cloudflare'
|
|
27
|
+
* import { tempo } from 'mppx/server'
|
|
28
|
+
*
|
|
29
|
+
* tempo.session({ account, bootstrap: true, currency, getClient, ...channels(env.MPPX_CHANNELS) })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function channels(namespace, options) {
|
|
33
|
+
const base = Store.durableObject(namespace, options);
|
|
34
|
+
const escrowContract = options?.escrowContract ?? tip20ChannelEscrow;
|
|
35
|
+
/** Removes `channelId` from the entry under `key`. */
|
|
36
|
+
async function remove(key, channelId) {
|
|
37
|
+
await base.update(key, (current) => {
|
|
38
|
+
const ids = lookupIds(current);
|
|
39
|
+
if (!ids.includes(channelId))
|
|
40
|
+
return noop;
|
|
41
|
+
const rest = ids.filter((id) => id !== channelId);
|
|
42
|
+
return rest.length
|
|
43
|
+
? { op: 'set', value: rest, result: undefined }
|
|
44
|
+
: { op: 'delete', result: undefined };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Adds `channelId` to the front of the entry under `key` while its stored channel still
|
|
49
|
+
* resumes, so a delayed add after its close leaves it out. A full entry drops channels that no
|
|
50
|
+
* longer resume before the oldest open one, keeping the most recent {@link maxLookupChannels}.
|
|
51
|
+
*/
|
|
52
|
+
async function add(key, channelId) {
|
|
53
|
+
for (let attempt = 1;; attempt++) {
|
|
54
|
+
const ids = lookupIds(await base.get(key));
|
|
55
|
+
if (ids.includes(channelId))
|
|
56
|
+
return;
|
|
57
|
+
if (!resumable(await base.get(channelId)))
|
|
58
|
+
return;
|
|
59
|
+
const full = ids.length >= maxLookupChannels;
|
|
60
|
+
const states = full ? await Promise.all(ids.map((id) => base.get(id))) : [];
|
|
61
|
+
const closed = new Set(ids.filter((_, i) => full && !resumable(states[i])));
|
|
62
|
+
const added = await base.update(key, (current) => {
|
|
63
|
+
const listed = lookupIds(current);
|
|
64
|
+
if (listed.includes(channelId))
|
|
65
|
+
return { op: 'noop', result: true };
|
|
66
|
+
// The entry changed since its channels were read: read them again, a few times at most.
|
|
67
|
+
if (attempt < maxLookupAttempts && !sameIds(listed, ids))
|
|
68
|
+
return { op: 'noop', result: false };
|
|
69
|
+
return {
|
|
70
|
+
op: 'set',
|
|
71
|
+
value: [channelId, ...listed.filter((id) => !closed.has(id))].slice(0, maxLookupChannels),
|
|
72
|
+
result: true,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
if (added)
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Moves the lookup entries of a channel from its previous state to its next one. Entries
|
|
81
|
+
* for the next state are written on every write, not only when they change: the channel
|
|
82
|
+
* commits before its entries, so a failed entry write is repaired by the channel's next
|
|
83
|
+
* write instead of staying missing. Writing an entry that is already there changes nothing.
|
|
84
|
+
* Best effort: the channel write has already committed, so a failed entry write must not
|
|
85
|
+
* report it as failed (a charge would apply without its Receipt).
|
|
86
|
+
*/
|
|
87
|
+
async function sync(previous, next) {
|
|
88
|
+
const before = lookupKeys(previous);
|
|
89
|
+
const after = lookupKeys(next);
|
|
90
|
+
await Promise.allSettled([
|
|
91
|
+
...before
|
|
92
|
+
.filter((key) => !after.includes(key))
|
|
93
|
+
.map((key) => remove(key, previous.channelId)),
|
|
94
|
+
...after.map((key) => add(key, next.channelId)),
|
|
95
|
+
]);
|
|
96
|
+
}
|
|
97
|
+
const store = {
|
|
98
|
+
...base,
|
|
99
|
+
async put(key, value) {
|
|
100
|
+
const previous = await base.get(key);
|
|
101
|
+
await base.put(key, value);
|
|
102
|
+
await sync(previous, value);
|
|
103
|
+
},
|
|
104
|
+
async delete(key) {
|
|
105
|
+
const previous = await base.get(key);
|
|
106
|
+
await base.delete(key);
|
|
107
|
+
await sync(previous, null);
|
|
108
|
+
},
|
|
109
|
+
async update(key, fn) {
|
|
110
|
+
// The last call of `fn` is the one the compare-and-swap committed.
|
|
111
|
+
const applied = {
|
|
112
|
+
previous: null,
|
|
113
|
+
};
|
|
114
|
+
const result = await base.update(key, (current) => {
|
|
115
|
+
applied.previous = current;
|
|
116
|
+
applied.change = fn(current);
|
|
117
|
+
return applied.change;
|
|
118
|
+
});
|
|
119
|
+
const { change, previous } = applied;
|
|
120
|
+
if (change && change.op !== 'noop')
|
|
121
|
+
await sync(previous, change.op === 'set' ? change.value : null);
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
const resolveChannelId = async ({ paymentRequest, source }) => {
|
|
126
|
+
const payer = source ? parsePkhSource(source) : null;
|
|
127
|
+
const { recipient } = paymentRequest;
|
|
128
|
+
if (!payer || !recipient)
|
|
129
|
+
return undefined;
|
|
130
|
+
if (paymentRequest.chainId !== undefined && paymentRequest.chainId !== payer.chainId)
|
|
131
|
+
return undefined;
|
|
132
|
+
const operator = paymentRequest.operator ?? zeroAddress;
|
|
133
|
+
const ids = lookupIds(await base.get(lookupKey({
|
|
134
|
+
chainId: payer.chainId,
|
|
135
|
+
escrowContract: paymentRequest.escrowContract ?? escrowContract,
|
|
136
|
+
payee: recipient,
|
|
137
|
+
operator,
|
|
138
|
+
payer: payer.address,
|
|
139
|
+
token: paymentRequest.currency,
|
|
140
|
+
})));
|
|
141
|
+
// An ID a delayed write left for a channel that has since closed resumes nothing.
|
|
142
|
+
for (const id of ids) {
|
|
143
|
+
const state = await base.get(id);
|
|
144
|
+
if (!resumable(state))
|
|
145
|
+
continue;
|
|
146
|
+
// mppx verifies a challenge without an operator against the zero operator, and only
|
|
147
|
+
// channels without one (legacy escrow channels) are not keyed by theirs.
|
|
148
|
+
if (isPrecompileState(state)
|
|
149
|
+
? state.operator.toLowerCase() !== operator.toLowerCase()
|
|
150
|
+
: paymentRequest.operator !== undefined)
|
|
151
|
+
continue;
|
|
152
|
+
return id;
|
|
153
|
+
}
|
|
154
|
+
return undefined;
|
|
155
|
+
};
|
|
156
|
+
return { resolveChannelId, store };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Settles the channels of a settlement queue batch on-chain, from the Worker's `queue()`
|
|
160
|
+
* handler. Each message is acknowledged once its channel settled or has nothing left to
|
|
161
|
+
* settle, and when settlement cannot succeed (the channel is gone or has no voucher); any
|
|
162
|
+
* other failure, such as a fee over the fee payer's cap, retries it after 30 s, doubling up
|
|
163
|
+
* to 10 minutes. The {@link ChannelStore} watchdog sends a channel again if its job never settles it.
|
|
164
|
+
*
|
|
165
|
+
* @example
|
|
166
|
+
* ```ts
|
|
167
|
+
* import { channels, settle } from 'mppx/cloudflare'
|
|
168
|
+
*
|
|
169
|
+
* export default {
|
|
170
|
+
* async queue(batch, env) {
|
|
171
|
+
* await settle(batch, { account, getClient, store: channels(env.MPPX_CHANNELS).store })
|
|
172
|
+
* },
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*/
|
|
176
|
+
export async function settle(batch, parameters) {
|
|
177
|
+
const { getClient, settle: settle_ = settleChannel, store, ...options } = parameters;
|
|
178
|
+
for (const message of batch.messages) {
|
|
179
|
+
try {
|
|
180
|
+
const channelId = message.body
|
|
181
|
+
?.channelId;
|
|
182
|
+
// A malformed job can never settle: drop it instead of failing the batch.
|
|
183
|
+
if (typeof channelId !== 'string') {
|
|
184
|
+
message.ack();
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const state = await store.get(channelId);
|
|
188
|
+
// A duplicate job, such as one the watchdog sent while the first was queued, has nothing left.
|
|
189
|
+
if (!isChannelState(state) || state.finalized || unsettledAmount(state) <= 0n) {
|
|
190
|
+
message.ack();
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
// The server's own cooperative close captures it: check again once that close lands or expires.
|
|
194
|
+
if (pendingClose(state)) {
|
|
195
|
+
message.retry({ delaySeconds: 60 });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Another job or a scheduled settlement is settling it: check again once that one is done.
|
|
199
|
+
const leaseOwner = await claim(store, channelId);
|
|
200
|
+
if (!leaseOwner) {
|
|
201
|
+
message.retry({ delaySeconds: 60 });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Waiting for the receipt can outlast the lease: keep it while the settlement runs, so no
|
|
205
|
+
// other job claims the channel and broadcasts the same voucher.
|
|
206
|
+
const renewal = setInterval(() => {
|
|
207
|
+
void renew(store, channelId, leaseOwner).catch(() => undefined);
|
|
208
|
+
}, settlementLeaseMs / 2);
|
|
209
|
+
try {
|
|
210
|
+
await settle_(store, await getClient({ chainId: state.chainId }), channelId, options);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
clearInterval(renewal);
|
|
214
|
+
await release(store, channelId, leaseOwner).catch(() => undefined);
|
|
215
|
+
}
|
|
216
|
+
message.ack();
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (isPermanent(error))
|
|
220
|
+
message.ack();
|
|
221
|
+
else
|
|
222
|
+
message.retry({ delaySeconds: Math.min(30 * 2 ** Math.max(0, message.attempts - 1), 600) });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const noop = { op: 'noop', result: undefined };
|
|
227
|
+
/** How long a settlement claim holds before another job may take over, as scheduled settlement's. */
|
|
228
|
+
const settlementLeaseMs = 5 * 60_000;
|
|
229
|
+
/**
|
|
230
|
+
* Claims a channel's settlement with the lease scheduled settlement takes, so overlapping jobs,
|
|
231
|
+
* or a job and a scheduled settlement, never broadcast the same voucher twice. Returns the
|
|
232
|
+
* lease's owner, or `undefined` while another one holds a live lease.
|
|
233
|
+
*/
|
|
234
|
+
async function claim(store, channelId) {
|
|
235
|
+
const owner = globalThis.crypto.randomUUID();
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
return store.update(channelId, (current) => {
|
|
238
|
+
if (!isChannelState(current))
|
|
239
|
+
return { op: 'noop', result: undefined };
|
|
240
|
+
const lease = current.scheduledSettlementLease;
|
|
241
|
+
if (lease && lease.expiresAt > now)
|
|
242
|
+
return { op: 'noop', result: undefined };
|
|
243
|
+
return {
|
|
244
|
+
op: 'set',
|
|
245
|
+
value: {
|
|
246
|
+
...current,
|
|
247
|
+
scheduledSettlementLease: { expiresAt: now + settlementLeaseMs, owner },
|
|
248
|
+
},
|
|
249
|
+
result: owner,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/** Extends a settlement lease {@link claim} took while its owner still holds it. */
|
|
254
|
+
async function renew(store, channelId, owner) {
|
|
255
|
+
await store.update(channelId, (current) => {
|
|
256
|
+
if (!isChannelState(current) || current.scheduledSettlementLease?.owner !== owner)
|
|
257
|
+
return noop;
|
|
258
|
+
return {
|
|
259
|
+
op: 'set',
|
|
260
|
+
value: {
|
|
261
|
+
...current,
|
|
262
|
+
scheduledSettlementLease: { expiresAt: Date.now() + settlementLeaseMs, owner },
|
|
263
|
+
},
|
|
264
|
+
result: undefined,
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
/** Releases a settlement lease {@link claim} took, unless another owner has taken it since. */
|
|
269
|
+
async function release(store, channelId, owner) {
|
|
270
|
+
await store.update(channelId, (current) => {
|
|
271
|
+
if (!isChannelState(current) || current.scheduledSettlementLease?.owner !== owner)
|
|
272
|
+
return noop;
|
|
273
|
+
const { scheduledSettlementLease: _, ...channel } = current;
|
|
274
|
+
return { op: 'set', value: channel, result: undefined };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Whether a settlement failed for good: the channel or its voucher is gone, the account cannot
|
|
279
|
+
* settle it, or the transaction confirmed and only recording it failed.
|
|
280
|
+
*/
|
|
281
|
+
function isPermanent(error) {
|
|
282
|
+
return (error instanceof ChannelNotFoundError ||
|
|
283
|
+
error instanceof VerificationFailedError ||
|
|
284
|
+
error instanceof SettlementCheckpointError);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The lookup key of a payer's channel to a payee, token, escrow, operator and chain. The operator
|
|
288
|
+
* is part of it so that channels opened with other operators cannot push a matching one out of
|
|
289
|
+
* the entry's {@link maxLookupChannels}.
|
|
290
|
+
*/
|
|
291
|
+
function lookupKey(lookup) {
|
|
292
|
+
const { chainId, escrowContract, operator, payee, payer, token } = lookup;
|
|
293
|
+
return `mppx:session-lookup:v3:${[payer, payee, token, escrowContract, operator].map((address) => address.toLowerCase()).join(':')}:${chainId}`;
|
|
294
|
+
}
|
|
295
|
+
/** Most channels a lookup entry keeps per payer, payee, token, escrow, operator and chain. */
|
|
296
|
+
const maxLookupChannels = 8;
|
|
297
|
+
/** Reads of a full lookup entry's channels before adding to it regardless of concurrent changes. */
|
|
298
|
+
const maxLookupAttempts = 3;
|
|
299
|
+
/** Whether two lookup entries list the same channels in the same order. */
|
|
300
|
+
function sameIds(a, b) {
|
|
301
|
+
return a.length === b.length && a.every((id, i) => id === b[i]);
|
|
302
|
+
}
|
|
303
|
+
/** The channel IDs of a lookup entry, most recent first. */
|
|
304
|
+
function lookupIds(entry) {
|
|
305
|
+
return Array.isArray(entry) ? entry.filter((id) => typeof id === 'string') : [];
|
|
306
|
+
}
|
|
307
|
+
/** Whether a payer may resume a stored channel: open, with no close requested. */
|
|
308
|
+
function resumable(state) {
|
|
309
|
+
return isChannelState(state) && !state.finalized && !state.closeRequestedAt;
|
|
310
|
+
}
|
|
311
|
+
/** The lookup keys of a channel a payer may resume. */
|
|
312
|
+
function lookupKeys(state) {
|
|
313
|
+
if (!resumable(state))
|
|
314
|
+
return [];
|
|
315
|
+
const signers = new Set([state.payer.toLowerCase(), state.authorizedSigner.toLowerCase()]);
|
|
316
|
+
return [...signers].map((payer) => lookupKey({
|
|
317
|
+
chainId: state.chainId,
|
|
318
|
+
escrowContract: state.escrowContract,
|
|
319
|
+
operator: isPrecompileState(state) ? state.operator : zeroAddress,
|
|
320
|
+
payee: state.payee,
|
|
321
|
+
payer,
|
|
322
|
+
token: state.token,
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=Channels.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Durable Object class that stores Tempo session channels and schedules their settlement. */
|
|
2
|
+
export { ChannelStore } from './ChannelStore.js';
|
|
3
|
+
/** Channel store and lookup for `tempo.session()`, and the settlement queue consumer. */
|
|
4
|
+
export { channels, settle } from './Channels.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Durable Object class that stores Tempo session channels and schedules their settlement. */
|
|
2
|
+
export { ChannelStore } from './ChannelStore.js';
|
|
3
|
+
/** Channel store and lookup for `tempo.session()`, and the settlement queue consumer. */
|
|
4
|
+
export { channels, settle } from './Channels.js';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
package/dist/identity/Client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Challenge from '../Challenge.js';
|
|
2
2
|
import * as Credential from '../Credential.js';
|
|
3
3
|
import * as SigningContext from './internal/SigningContext.js';
|
|
4
|
+
const skipPayment = Symbol.for('mppx.fetch.skipPayment');
|
|
4
5
|
/** x402 credential headers (v2 and v1); a request carrying one is a paid attempt. */
|
|
5
6
|
const x402CredentialHeaders = ['payment-signature', 'x-payment'];
|
|
6
7
|
/**
|
|
@@ -99,7 +100,12 @@ export function wrapFetch(fetch, client, context) {
|
|
|
99
100
|
// A retry may answer a refreshed challenge: bind proofs to the challenge its credential
|
|
100
101
|
// carries, and let provers prepare for a refreshed 402 of the same method and intent.
|
|
101
102
|
const challenge = credentialChallenge(request.headers) ?? context?.challenge;
|
|
102
|
-
const { response } = await client.send(
|
|
103
|
+
const { response } = await client.send((input, options) => fetch(input, {
|
|
104
|
+
...options,
|
|
105
|
+
...(init?.[skipPayment]
|
|
106
|
+
? { [skipPayment]: true }
|
|
107
|
+
: {}),
|
|
108
|
+
}), request, { ...context, challenge });
|
|
103
109
|
if (response.status === 402 && challenge)
|
|
104
110
|
// A 402 without a parseable Payment challenge is returned as is: the caller decides.
|
|
105
111
|
for (const next of challengesOf(response))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** An event dispatched from a `text/event-stream` body. */
|
|
2
|
+
export type Event = {
|
|
3
|
+
/** The event's data: its `data` field values joined by `\n`. */
|
|
4
|
+
data: string;
|
|
5
|
+
/** The event's type: its last `event` field value, or `'message'` when it has none. */
|
|
6
|
+
event: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates an incremental parser for a `text/event-stream` body, as the WHATWG HTML
|
|
10
|
+
* spec interprets one. Feed it decoded text chunks split anywhere; it calls `onEvent`
|
|
11
|
+
* for each event a blank line ends. Lines end in `\n`, `\r\n` or `\r`; comments (`:`),
|
|
12
|
+
* `id`, `retry` and unknown fields are ignored, and an event that never ends is not
|
|
13
|
+
* dispatched.
|
|
14
|
+
*
|
|
15
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
16
|
+
*/
|
|
17
|
+
export declare function createParser(onEvent: (event: Event) => void): {
|
|
18
|
+
/** Parses the next chunk of the stream. */
|
|
19
|
+
feed(chunk: string): void;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an incremental parser for a `text/event-stream` body, as the WHATWG HTML
|
|
3
|
+
* spec interprets one. Feed it decoded text chunks split anywhere; it calls `onEvent`
|
|
4
|
+
* for each event a blank line ends. Lines end in `\n`, `\r\n` or `\r`; comments (`:`),
|
|
5
|
+
* `id`, `retry` and unknown fields are ignored, and an event that never ends is not
|
|
6
|
+
* dispatched.
|
|
7
|
+
*
|
|
8
|
+
* @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
|
9
|
+
*/
|
|
10
|
+
export function createParser(onEvent) {
|
|
11
|
+
let buffer = '';
|
|
12
|
+
let started = false;
|
|
13
|
+
let skipLineFeed = false;
|
|
14
|
+
let data;
|
|
15
|
+
let event = '';
|
|
16
|
+
function parseLine(line) {
|
|
17
|
+
if (line === '') {
|
|
18
|
+
if (data !== undefined)
|
|
19
|
+
onEvent({ data, event: event || 'message' });
|
|
20
|
+
data = undefined;
|
|
21
|
+
event = '';
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const colon = line.indexOf(':');
|
|
25
|
+
if (colon === 0)
|
|
26
|
+
return;
|
|
27
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
28
|
+
if (field !== 'data' && field !== 'event')
|
|
29
|
+
return;
|
|
30
|
+
let value = '';
|
|
31
|
+
if (colon !== -1)
|
|
32
|
+
value = line.slice(line.charCodeAt(colon + 1) === 32 ? colon + 2 : colon + 1);
|
|
33
|
+
if (field === 'event')
|
|
34
|
+
event = value;
|
|
35
|
+
else
|
|
36
|
+
data = data === undefined ? value : data + '\n' + value;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
/** Parses the next chunk of the stream. */
|
|
40
|
+
feed(chunk) {
|
|
41
|
+
if (!started && chunk) {
|
|
42
|
+
started = true;
|
|
43
|
+
if (chunk.startsWith('\uFEFF'))
|
|
44
|
+
chunk = chunk.slice(1);
|
|
45
|
+
}
|
|
46
|
+
// A `\r` ending the last chunk may be the first half of a `\r\n`.
|
|
47
|
+
if (skipLineFeed && chunk) {
|
|
48
|
+
skipLineFeed = false;
|
|
49
|
+
if (chunk.startsWith('\n'))
|
|
50
|
+
chunk = chunk.slice(1);
|
|
51
|
+
}
|
|
52
|
+
buffer += chunk;
|
|
53
|
+
if (chunk.indexOf('\n') === -1 && chunk.indexOf('\r') === -1)
|
|
54
|
+
return;
|
|
55
|
+
// Scan with indexOf rather than a global RegExp: it is several times faster on
|
|
56
|
+
// line-dense bodies.
|
|
57
|
+
let start = 0;
|
|
58
|
+
let lineFeed = buffer.indexOf('\n');
|
|
59
|
+
let carriageReturn = buffer.indexOf('\r');
|
|
60
|
+
while (lineFeed !== -1 || carriageReturn !== -1) {
|
|
61
|
+
const end = lineFeed === -1
|
|
62
|
+
? carriageReturn
|
|
63
|
+
: carriageReturn === -1
|
|
64
|
+
? lineFeed
|
|
65
|
+
: Math.min(lineFeed, carriageReturn);
|
|
66
|
+
parseLine(buffer.slice(start, end));
|
|
67
|
+
start = end + 1;
|
|
68
|
+
if (end === carriageReturn) {
|
|
69
|
+
if (start === buffer.length)
|
|
70
|
+
skipLineFeed = true;
|
|
71
|
+
else if (lineFeed === start)
|
|
72
|
+
start++;
|
|
73
|
+
carriageReturn = buffer.indexOf('\r', start);
|
|
74
|
+
}
|
|
75
|
+
if (lineFeed !== -1 && lineFeed < start)
|
|
76
|
+
lineFeed = buffer.indexOf('\n', start);
|
|
77
|
+
}
|
|
78
|
+
buffer = buffer.slice(start);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Current mppx package version. */
|
|
2
|
-
export declare const version = "0.17.
|
|
2
|
+
export declare const version = "0.17.4";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.17.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.17.4";
|
|
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.17.
|
|
3
|
+
export const version = '0.17.4';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -70,8 +70,8 @@ export declare namespace wrap {
|
|
|
70
70
|
/** Array of methods to use. Accepts individual clients or tuples (e.g. from `tempo()`). */
|
|
71
71
|
methods: methods;
|
|
72
72
|
/**
|
|
73
|
-
* Realms allowed to request zero-amount Tempo proofs. An MCP server has no origin that could
|
|
74
|
-
* own a realm, so these
|
|
73
|
+
* Realms allowed to request zero-amount Tempo proofs or session payments. An MCP server has no origin that could
|
|
74
|
+
* own a realm, so these signatures are refused unless their realm is listed here.
|
|
75
75
|
*/
|
|
76
76
|
trustedTempoProofRealms?: readonly string[] | undefined;
|
|
77
77
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as Challenge from '../../Challenge.js';
|
|
2
2
|
import * as ProofRealm from '../../client/internal/ProofRealm.js';
|
|
3
|
+
import { adapterPaymentMetaKey } from '../../client/internal/protocols/Mcp.js';
|
|
3
4
|
import * as Credential from '../../Credential.js';
|
|
4
5
|
import * as Expires from '../../Expires.js';
|
|
5
6
|
import * as AcceptPayment from '../../internal/AcceptPayment.js';
|
|
@@ -57,9 +58,11 @@ export function wrap(client, config) {
|
|
|
57
58
|
enumerable: false,
|
|
58
59
|
value: (params, resultSchema, options) => {
|
|
59
60
|
const { context, onPaymentRequired, ...requestOptions } = options ?? {};
|
|
60
|
-
return callTool(params, {
|
|
61
|
+
return callTool({ ...params, _meta: { ...params._meta, [adapterPaymentMetaKey]: true } }, {
|
|
61
62
|
context,
|
|
62
|
-
onPaymentRequired: onPaymentRequired === null
|
|
63
|
+
onPaymentRequired: onPaymentRequired === null
|
|
64
|
+
? undefined
|
|
65
|
+
: (onPaymentRequired ?? config.onPaymentRequired),
|
|
63
66
|
requestOptions: Object.keys(requestOptions).length
|
|
64
67
|
? requestOptions
|
|
65
68
|
: undefined,
|
package/dist/proxy/Proxy.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export declare namespace create {
|
|
|
49
49
|
docs?: Service.Docs | undefined;
|
|
50
50
|
/** Custom `fetch` implementation. Defaults to `globalThis.fetch`. */
|
|
51
51
|
fetch?: typeof globalThis.fetch | undefined;
|
|
52
|
+
/** Maximum request body bytes on every route, including free routes. Defaults to 10 MiB. */
|
|
53
|
+
maxRequestBodySize?: number | undefined;
|
|
52
54
|
/** Default handler for failed upstream attempts. Services may override it. */
|
|
53
55
|
onUpstreamError?: Service.UpstreamErrorHandler | undefined;
|
|
54
56
|
/**
|
package/dist/proxy/Proxy.js
CHANGED
|
@@ -34,6 +34,9 @@ import * as Service from './Service.js';
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
export function create(config) {
|
|
37
|
+
const maxRequestBodySize = config.maxRequestBodySize ?? 10 * 1024 * 1024;
|
|
38
|
+
if (!Number.isSafeInteger(maxRequestBodySize) || maxRequestBodySize < 0)
|
|
39
|
+
throw new RangeError('maxRequestBodySize must be a non-negative safe integer');
|
|
37
40
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
38
41
|
const replay = config.replay ? Replay.resolve(config.replay) : undefined;
|
|
39
42
|
const services = new Map(config.services.map((s) => {
|
|
@@ -100,8 +103,22 @@ export function create(config) {
|
|
|
100
103
|
if (!matched)
|
|
101
104
|
return new Response('Not Found', { status: 404 });
|
|
102
105
|
const endpoint = matched.value;
|
|
103
|
-
const ctx = { request, service, upstreamPath };
|
|
104
106
|
const handler = Service.getHandler(endpoint);
|
|
107
|
+
const declaredLength = Number(request.headers.get('content-length'));
|
|
108
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxRequestBodySize)
|
|
109
|
+
return new Response('Payload Too Large', { status: 413 });
|
|
110
|
+
const paymentCredential = handler
|
|
111
|
+
? Replay.credentialOf(request, credentialHeaders(handler))
|
|
112
|
+
: undefined;
|
|
113
|
+
let boundedBody;
|
|
114
|
+
if ((!handler || paymentCredential) && request.body) {
|
|
115
|
+
const body = await Replay.readBody(request, maxRequestBodySize);
|
|
116
|
+
if (!body)
|
|
117
|
+
return new Response('Payload Too Large', { status: 413 });
|
|
118
|
+
boundedBody = body;
|
|
119
|
+
request = Replay.withBody(request, body);
|
|
120
|
+
}
|
|
121
|
+
const ctx = { request, service, upstreamPath };
|
|
105
122
|
if (!handler)
|
|
106
123
|
return proxyUpstream({
|
|
107
124
|
request,
|
|
@@ -116,11 +133,12 @@ export function create(config) {
|
|
|
116
133
|
let replayBody;
|
|
117
134
|
let replayKey;
|
|
118
135
|
// Routes with identity verifiers keep paying normally: a replay would skip them.
|
|
119
|
-
const credential = replay && !fallbackMatch && !requiresIdentity(handler)
|
|
120
|
-
? Replay.credentialOf(request, credentialHeaders(handler))
|
|
121
|
-
: undefined;
|
|
136
|
+
const credential = replay && !fallbackMatch && !requiresIdentity(handler) ? paymentCredential : undefined;
|
|
122
137
|
if (replay && credential) {
|
|
123
|
-
const body =
|
|
138
|
+
const body = boundedBody ??
|
|
139
|
+
(await Replay.readBody(request, Math.min(maxRequestBodySize, replay.maxRequestBodySize)));
|
|
140
|
+
if (body && body.length > replay.maxRequestBodySize)
|
|
141
|
+
return new Response('Payload Too Large', { status: 413 });
|
|
124
142
|
if (!body)
|
|
125
143
|
return new Response('Payload Too Large', { status: 413 });
|
|
126
144
|
replayBody = body;
|
|
@@ -179,6 +197,13 @@ export function create(config) {
|
|
|
179
197
|
return managementResponse;
|
|
180
198
|
if (fallbackMatch)
|
|
181
199
|
return new Response('Method Not Allowed', { status: 405 });
|
|
200
|
+
if (!boundedBody && !replayBody && request.body) {
|
|
201
|
+
const body = await Replay.readBody(request, maxRequestBodySize);
|
|
202
|
+
if (!body)
|
|
203
|
+
return result.withReceipt(new Response('Payload Too Large', { status: 413 }));
|
|
204
|
+
request = Replay.withBody(request, body);
|
|
205
|
+
ctx.request = request;
|
|
206
|
+
}
|
|
182
207
|
const options = Service.getOptions(endpoint);
|
|
183
208
|
if (replay && replayKey && replayBody) {
|
|
184
209
|
const body = replayBody;
|
|
@@ -211,7 +236,7 @@ export function create(config) {
|
|
|
211
236
|
}
|
|
212
237
|
return {
|
|
213
238
|
fetch: handle,
|
|
214
|
-
listener: Request.toNodeListener(handle),
|
|
239
|
+
listener: Request.toNodeListener(handle, { maxBodySize: maxRequestBodySize }),
|
|
215
240
|
};
|
|
216
241
|
}
|
|
217
242
|
/** @internal */
|
package/dist/server/Mppx.js
CHANGED
|
@@ -1876,11 +1876,12 @@ function composeHandlers(handlers, composeOptions, selectOffers) {
|
|
|
1876
1876
|
}
|
|
1877
1877
|
mergedHeaders.set('Content-Type', 'text/html; charset=utf-8');
|
|
1878
1878
|
const firstData = Object.values(dataMap)[0];
|
|
1879
|
+
const entries = await Promise.all(htmlEntries.map(async (entry) => ({
|
|
1880
|
+
challenge: entry.challenge,
|
|
1881
|
+
content: await Html.resolveContent(entry.handler._internal.html.content),
|
|
1882
|
+
})));
|
|
1879
1883
|
const body = Html.render({
|
|
1880
|
-
entries
|
|
1881
|
-
challenge: entry.challenge,
|
|
1882
|
-
content: entry.handler._internal.html.content,
|
|
1883
|
-
})),
|
|
1884
|
+
entries,
|
|
1884
1885
|
dataMap,
|
|
1885
1886
|
formattedAmount: firstData.formattedAmount,
|
|
1886
1887
|
panels: true,
|
package/dist/server/Transport.js
CHANGED
|
@@ -98,7 +98,7 @@ export function http(options = {}) {
|
|
|
98
98
|
},
|
|
99
99
|
};
|
|
100
100
|
return Html.render({
|
|
101
|
-
entries: [{ challenge, content: options.html.content }],
|
|
101
|
+
entries: [{ challenge, content: await Html.resolveContent(options.html.content) }],
|
|
102
102
|
dataMap,
|
|
103
103
|
formattedAmount: amount,
|
|
104
104
|
text,
|
|
@@ -2,7 +2,11 @@ import type * as Challenge from '../../../Challenge.js';
|
|
|
2
2
|
import type * as Method from '../../../Method.js';
|
|
3
3
|
export type Options = {
|
|
4
4
|
config: Record<string, unknown>;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The payment page's script, or a function that loads it. A loader lets a
|
|
7
|
+
* method import its (large) page bundle only when a page is rendered.
|
|
8
|
+
*/
|
|
9
|
+
content: string | (() => Promise<string>);
|
|
6
10
|
formatAmount: (request: any) => string | Promise<string>;
|
|
7
11
|
text: Text | undefined;
|
|
8
12
|
theme: Theme | undefined;
|
|
@@ -21,6 +25,8 @@ export type Data<method extends Method.Method = Method.Method, config extends Re
|
|
|
21
25
|
};
|
|
22
26
|
};
|
|
23
27
|
export { attrs, classNames, ids, params } from './constants.js';
|
|
28
|
+
/** Resolves {@link Options.content}, loading it when it is a loader function. */
|
|
29
|
+
export declare function resolveContent(content: Options['content']): Promise<string>;
|
|
24
30
|
declare class CssVar {
|
|
25
31
|
readonly name: string;
|
|
26
32
|
constructor(token: string);
|