mppx 0.17.1 → 0.17.3
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 +28 -0
- package/dist/Store.d.ts +225 -0
- package/dist/Store.js +358 -0
- package/dist/cli/cli.js +95 -94
- package/dist/cli/plugins/index.js +12 -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/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/identity/Client.js +7 -1
- package/dist/internal/sse.d.ts +21 -0
- package/dist/internal/sse.js +65 -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/internal/html.gen.js +1 -1
- package/dist/tempo/internal/fee-payer.d.ts +85 -10
- package/dist/tempo/internal/fee-payer.js +183 -19
- package/dist/tempo/legacy/session/Chain.js +2 -0
- package/dist/tempo/server/Charge.js +2 -2
- 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 +9 -0
- package/dist/tempo/session/server/ChannelStore.js +8 -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/zod.d.ts +2 -1
- package/dist/zod.js +1 -1
- package/package.json +1 -3
- package/dist/stripe/server/internal/html/types.d.ts +0 -2
- package/dist/stripe/server/internal/html/types.js +0 -2
|
@@ -1,7 +1,58 @@
|
|
|
1
1
|
import * as Constants from '../../../Constants.js';
|
|
2
2
|
import * as Credential from '../../../Credential.js';
|
|
3
3
|
import { createSessionReceipt, extractData, formatMessageEvent, formatNeedVoucherEvent, formatReceiptEvent, readSessionChallengeAmount, requireSessionCredentialContext, } from '../precompile/Protocol.js';
|
|
4
|
+
import * as ChannelStore from './ChannelStore.js';
|
|
4
5
|
import { meterIterable, } from './MeteredStream.js';
|
|
6
|
+
/** Exposes durable retry information when no refund hook is configured or it fails. */
|
|
7
|
+
export class PrepaidRefundError extends Error {
|
|
8
|
+
refund;
|
|
9
|
+
name = 'PrepaidRefundError';
|
|
10
|
+
constructor(refund) {
|
|
11
|
+
super('Prepaid stream delivery failed and requires merchant refund reconciliation.');
|
|
12
|
+
this.refund = refund;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Maximum retained automatic refund operations per channel. Never evict live retry keys. */
|
|
16
|
+
export const maxPrepaidRefunds = 128;
|
|
17
|
+
/**
|
|
18
|
+
* Persist a delivery obligation before returning a managed response. Failed hook deliveries remain
|
|
19
|
+
* in `channel.prepaidRefunds` for a merchant worker. Pending entries require manual delivery
|
|
20
|
+
* reconciliation; failed entries may retry the credit with their original terms within 24 hours.
|
|
21
|
+
* Acknowledging the hook means the merchant has durably accepted responsibility for the job.
|
|
22
|
+
*/
|
|
23
|
+
export async function preparePrepaidRefund(options) {
|
|
24
|
+
if (!options.autoCreditPrepaid || !options.prepaidUnits || options.tickCost === 0n)
|
|
25
|
+
return;
|
|
26
|
+
if (!options.refundIdempotencyKey)
|
|
27
|
+
throw new Error('Automatic refunds require a stable refundIdempotencyKey');
|
|
28
|
+
const key = options.refundIdempotencyKey;
|
|
29
|
+
const amount = (BigInt(options.prepaidUnits) * options.tickCost).toString();
|
|
30
|
+
await options.store.updateChannel(options.channelId, (current) => {
|
|
31
|
+
if (!current)
|
|
32
|
+
throw new Error('Cannot prepare prepaid refund: channel not found');
|
|
33
|
+
const existing = current.prepaidRefunds?.[key];
|
|
34
|
+
if (existing) {
|
|
35
|
+
if (existing.amount !== amount || existing.units !== options.prepaidUnits)
|
|
36
|
+
throw new Error('Prepaid refund key conflicts with an existing delivery');
|
|
37
|
+
return current;
|
|
38
|
+
}
|
|
39
|
+
const liveMarkers = Object.values(current.idempotencyMarkers ?? {}).filter((marker) => marker.expiresAt > Date.now()).length;
|
|
40
|
+
if (Object.keys(current.prepaidRefunds ?? {}).length + liveMarkers >= maxPrepaidRefunds)
|
|
41
|
+
throw new Error('Prepaid refund capacity reached; reconcile outstanding deliveries');
|
|
42
|
+
return {
|
|
43
|
+
...current,
|
|
44
|
+
prepaidRefunds: {
|
|
45
|
+
...current.prepaidRefunds,
|
|
46
|
+
[key]: {
|
|
47
|
+
amount,
|
|
48
|
+
units: options.prepaidUnits,
|
|
49
|
+
status: 'pending',
|
|
50
|
+
createdAt: Date.now(),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
}
|
|
5
56
|
/**
|
|
6
57
|
* SSE (Server-Sent Events) utilities for metered streaming payments.
|
|
7
58
|
*
|
|
@@ -30,6 +81,9 @@ export { extractData, formatMessageEvent, formatNeedVoucherEvent, formatReceiptE
|
|
|
30
81
|
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
31
82
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
32
83
|
*
|
|
84
|
+
* With `autoCreditPrepaid`, a failure before a prepaid item is enqueued attempts an idempotent credit.
|
|
85
|
+
* If credit is declined or uncertain, `onRefundRequired` handles merchant reconciliation.
|
|
86
|
+
*
|
|
33
87
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
34
88
|
*/
|
|
35
89
|
export function serve(options) {
|
|
@@ -38,11 +92,100 @@ export function serve(options) {
|
|
|
38
92
|
const abortController = new AbortController();
|
|
39
93
|
let streamController;
|
|
40
94
|
let terminal = false;
|
|
95
|
+
let preparation = Promise.resolve();
|
|
96
|
+
let termination;
|
|
97
|
+
let committedPrepaidAwaitingDelivery = 0;
|
|
98
|
+
let undeliveredPrepaidUnits = options.prepaidUnits ?? 0;
|
|
99
|
+
if (!Number.isSafeInteger(undeliveredPrepaidUnits) || undeliveredPrepaidUnits < 0)
|
|
100
|
+
throw new Error('prepaidUnits must be a non-negative safe integer');
|
|
101
|
+
const refundIdempotencyKey = options.refundIdempotencyKey ?? `stream:${globalThis.crypto.randomUUID()}`;
|
|
102
|
+
ChannelStore.assertIdempotencyOptions({ idempotencyKey: refundIdempotencyKey });
|
|
103
|
+
const refundOptions = { ...options, refundIdempotencyKey };
|
|
104
|
+
const clearObligation = async () => {
|
|
105
|
+
if (!options.autoCreditPrepaid)
|
|
106
|
+
return;
|
|
107
|
+
await store.updateChannel(channelId, (current) => {
|
|
108
|
+
if (!current?.prepaidRefunds?.[refundIdempotencyKey])
|
|
109
|
+
return current;
|
|
110
|
+
const prepaidRefunds = { ...current.prepaidRefunds };
|
|
111
|
+
delete prepaidRefunds[refundIdempotencyKey];
|
|
112
|
+
return { ...current, prepaidRefunds };
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
const creditFailedDelivery = async () => {
|
|
116
|
+
if (!options.autoCreditPrepaid)
|
|
117
|
+
return;
|
|
118
|
+
if (undeliveredPrepaidUnits === 0 || tickCost === 0n) {
|
|
119
|
+
await clearObligation();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const recordFailure = await store
|
|
123
|
+
.updateChannel(channelId, (current) => {
|
|
124
|
+
if (!current?.prepaidRefunds?.[refundIdempotencyKey])
|
|
125
|
+
throw new Error('Prepaid refund obligation missing');
|
|
126
|
+
return {
|
|
127
|
+
...current,
|
|
128
|
+
prepaidRefunds: {
|
|
129
|
+
...current.prepaidRefunds,
|
|
130
|
+
[refundIdempotencyKey]: {
|
|
131
|
+
...current.prepaidRefunds[refundIdempotencyKey],
|
|
132
|
+
status: 'failed',
|
|
133
|
+
amount: (BigInt(undeliveredPrepaidUnits) * tickCost).toString(),
|
|
134
|
+
units: undeliveredPrepaidUnits,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
.then(() => undefined, (error) => ({ error }));
|
|
140
|
+
const amount = BigInt(undeliveredPrepaidUnits) * tickCost;
|
|
141
|
+
const result = recordFailure
|
|
142
|
+
? { status: 'failed', error: recordFailure.error }
|
|
143
|
+
: await ChannelStore.creditChannel(store, channelId, amount, {
|
|
144
|
+
idempotencyKey: refundIdempotencyKey,
|
|
145
|
+
units: undeliveredPrepaidUnits,
|
|
146
|
+
maxMarkers: maxPrepaidRefunds,
|
|
147
|
+
}).catch((error) => ({ status: 'failed', error }));
|
|
148
|
+
if (result.status === 'credited') {
|
|
149
|
+
await clearObligation();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
let externalRefundAmount = null;
|
|
153
|
+
if (result.status === 'declined' && result.reason === 'finalized') {
|
|
154
|
+
const captured = result.channel.settledOnChain - (result.channel.spent - amount);
|
|
155
|
+
externalRefundAmount = captured < 0n ? 0n : captured > amount ? amount : captured;
|
|
156
|
+
if (externalRefundAmount === 0n) {
|
|
157
|
+
await clearObligation();
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const refund = {
|
|
162
|
+
channelId,
|
|
163
|
+
amount,
|
|
164
|
+
externalRefundAmount,
|
|
165
|
+
units: undeliveredPrepaidUnits,
|
|
166
|
+
idempotencyKey: refundIdempotencyKey,
|
|
167
|
+
credit: result,
|
|
168
|
+
};
|
|
169
|
+
if (!options.onRefundRequired)
|
|
170
|
+
throw new PrepaidRefundError(refund);
|
|
171
|
+
try {
|
|
172
|
+
await options.onRefundRequired(refund);
|
|
173
|
+
await clearObligation();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
throw new PrepaidRefundError(refund);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
41
179
|
const iterator = meterIterable({
|
|
42
180
|
store,
|
|
43
181
|
channelId,
|
|
44
182
|
tickCost,
|
|
45
183
|
generate,
|
|
184
|
+
chargeOnce: options.chargeOnce,
|
|
185
|
+
onPrepaidChargeCommitted() {
|
|
186
|
+
undeliveredPrepaidUnits = Math.max(0, undeliveredPrepaidUnits - 1);
|
|
187
|
+
committedPrepaidAwaitingDelivery += 1;
|
|
188
|
+
},
|
|
46
189
|
beforeCharge: options.beforeCharge,
|
|
47
190
|
onChargeCommitted: options.onChargeCommitted,
|
|
48
191
|
pollIntervalMs,
|
|
@@ -54,23 +197,33 @@ export function serve(options) {
|
|
|
54
197
|
const cleanup = () => signal?.removeEventListener('abort', abort);
|
|
55
198
|
const terminate = (reason) => {
|
|
56
199
|
if (terminal)
|
|
57
|
-
return;
|
|
200
|
+
return termination;
|
|
58
201
|
terminal = true;
|
|
59
202
|
abortController.abort(reason);
|
|
60
203
|
cleanup();
|
|
61
204
|
void Promise.resolve()
|
|
62
205
|
.then(() => iterator.return?.(undefined))
|
|
63
206
|
.catch(() => { });
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
207
|
+
termination = preparation
|
|
208
|
+
.catch(() => { })
|
|
209
|
+
.then(clearObligation)
|
|
210
|
+
.finally(() => {
|
|
211
|
+
try {
|
|
212
|
+
streamController?.close();
|
|
213
|
+
}
|
|
214
|
+
catch { }
|
|
215
|
+
});
|
|
216
|
+
return termination;
|
|
217
|
+
};
|
|
218
|
+
const abort = () => {
|
|
219
|
+
void terminate(signal?.reason)?.catch(() => { });
|
|
68
220
|
};
|
|
69
|
-
const abort = () => terminate(signal?.reason);
|
|
70
221
|
signal?.addEventListener('abort', abort, { once: true });
|
|
71
222
|
return new ReadableStream({
|
|
72
|
-
start(controller) {
|
|
223
|
+
async start(controller) {
|
|
73
224
|
streamController = controller;
|
|
225
|
+
preparation = preparePrepaidRefund(refundOptions);
|
|
226
|
+
await preparation;
|
|
74
227
|
if (signal?.aborted)
|
|
75
228
|
abort();
|
|
76
229
|
},
|
|
@@ -86,8 +239,13 @@ export function serve(options) {
|
|
|
86
239
|
return;
|
|
87
240
|
const value = next.value;
|
|
88
241
|
controller.enqueue(encoder.encode(formatMessageEvent(value)));
|
|
242
|
+
if (committedPrepaidAwaitingDelivery > 0)
|
|
243
|
+
committedPrepaidAwaitingDelivery -= 1;
|
|
244
|
+
else
|
|
245
|
+
undeliveredPrepaidUnits = Math.max(0, undeliveredPrepaidUnits - 1);
|
|
89
246
|
return;
|
|
90
247
|
}
|
|
248
|
+
await clearObligation();
|
|
91
249
|
if (!abortController.signal.aborted) {
|
|
92
250
|
const channel = await store.getChannel(channelId);
|
|
93
251
|
if (channel) {
|
|
@@ -114,14 +272,21 @@ export function serve(options) {
|
|
|
114
272
|
return;
|
|
115
273
|
terminal = true;
|
|
116
274
|
cleanup();
|
|
117
|
-
if (!abortController.signal.aborted)
|
|
118
|
-
|
|
275
|
+
if (!abortController.signal.aborted) {
|
|
276
|
+
try {
|
|
277
|
+
await creditFailedDelivery();
|
|
278
|
+
controller.error(e);
|
|
279
|
+
}
|
|
280
|
+
catch (refundError) {
|
|
281
|
+
controller.error(new AggregateError([e, refundError], 'Stream delivery and prepaid refund failed.'));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
119
284
|
else
|
|
120
285
|
controller.close();
|
|
121
286
|
}
|
|
122
287
|
},
|
|
123
288
|
cancel(reason) {
|
|
124
|
-
terminate(reason);
|
|
289
|
+
return terminate(reason);
|
|
125
290
|
},
|
|
126
291
|
});
|
|
127
292
|
}
|
package/dist/zod.d.ts
CHANGED
package/dist/zod.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mppx",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.17.
|
|
4
|
+
"version": "0.17.3",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://github.com/wevm/mppx#readme",
|
|
@@ -206,8 +206,6 @@
|
|
|
206
206
|
}
|
|
207
207
|
},
|
|
208
208
|
"dependencies": {
|
|
209
|
-
"@stripe/stripe-js": "9.17.0",
|
|
210
|
-
"eventsource-parser": "4.1.1",
|
|
211
209
|
"ox": "0.14.45",
|
|
212
210
|
"structured-headers": "2.1.0",
|
|
213
211
|
"zod": "^4.6.7"
|