mppx 0.11.0 → 0.13.1
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 +83 -0
- package/README.md +22 -1
- 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/agent.d.ts +3 -0
- package/dist/cli/agent.js +20 -0
- package/dist/cli/cli.d.ts +9 -1
- 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/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 +63 -24
- 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 +8 -2
- 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/evm/server/Methods.d.ts +164 -3
- package/dist/evm/server/Methods.js +50 -2
- 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 +18 -2
- package/dist/server/Mppx.js +204 -92
- 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/server/internal/html/compose.main.gen.d.ts +1 -1
- package/dist/server/internal/html/serviceWorker.gen.d.ts +1 -1
- package/dist/stripe/server/Methods.d.ts +10 -3
- package/dist/stripe/server/Methods.js +19 -6
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- 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/currencies.d.ts +19 -0
- package/dist/tempo/internal/currencies.js +41 -0
- package/dist/tempo/internal/defaults.d.ts +5 -3
- package/dist/tempo/internal/defaults.js +6 -2
- package/dist/tempo/internal/fee-payer.d.ts +11 -7
- package/dist/tempo/internal/fee-payer.js +30 -11
- 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.d.ts +12 -1
- package/dist/tempo/server/Charge.js +109 -78
- package/dist/tempo/server/Methods.d.ts +5048 -40
- package/dist/tempo/server/Methods.js +104 -13
- package/dist/tempo/server/Subscription.d.ts +1 -1
- package/dist/tempo/server/Subscription.js +31 -4
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- 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 +22 -2
- package/dist/tempo/session/server/ChannelStore.js +37 -7
- package/dist/tempo/session/server/CredentialVerification.d.ts +6 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -19
- package/dist/tempo/session/server/MeteredStream.d.ts +3 -2
- package/dist/tempo/session/server/MeteredStream.js +85 -29
- package/dist/tempo/session/server/RequestState.d.ts +6 -0
- package/dist/tempo/session/server/RequestState.js +11 -4
- package/dist/tempo/session/server/Session.d.ts +2 -1
- package/dist/tempo/session/server/Session.js +10 -2
- package/dist/tempo/session/server/Settlement.d.ts +23 -0
- package/dist/tempo/session/server/Settlement.js +105 -25
- 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/KeyAuthorization.d.ts +70 -8
- 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 +8 -8
- 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
|
@@ -49,6 +49,7 @@ export function sse(options) {
|
|
|
49
49
|
const base = Transport.http();
|
|
50
50
|
return Transport.from({
|
|
51
51
|
name: 'sse',
|
|
52
|
+
supportsStreamingReceipts: true,
|
|
52
53
|
captureRequest(request) {
|
|
53
54
|
return (base.captureRequest?.(request) ?? {
|
|
54
55
|
headers: new Headers(request.headers),
|
|
@@ -63,7 +64,7 @@ export function sse(options) {
|
|
|
63
64
|
respondChallenge(options) {
|
|
64
65
|
return base.respondChallenge(options);
|
|
65
66
|
},
|
|
66
|
-
respondReceipt({ credential, envelope, receipt, response, challengeId, input }) {
|
|
67
|
+
async respondReceipt({ credential, envelope, receipt, response, challengeId, input }) {
|
|
67
68
|
const verifiedCredential = envelope?.credential ?? credential;
|
|
68
69
|
const verifiedChallengeId = envelope?.challenge.id ?? challengeId;
|
|
69
70
|
const verifiedRequest = envelope?.request ?? verifiedCredential.challenge.request;
|
|
@@ -112,8 +113,7 @@ export function sse(options) {
|
|
|
112
113
|
if (hasPrepaidSessionTick(currentReceipt)) {
|
|
113
114
|
return baseResponse;
|
|
114
115
|
}
|
|
115
|
-
const
|
|
116
|
-
if (available < tickCost) {
|
|
116
|
+
const paymentRequired = (available) => {
|
|
117
117
|
const error = new Errors.InsufficientBalanceError({
|
|
118
118
|
reason: `requested ${tickCost}, available ${available}`,
|
|
119
119
|
});
|
|
@@ -125,7 +125,10 @@ export function sse(options) {
|
|
|
125
125
|
'Content-Type': 'application/problem+json',
|
|
126
126
|
},
|
|
127
127
|
});
|
|
128
|
-
}
|
|
128
|
+
};
|
|
129
|
+
const available = BigInt(currentReceipt.acceptedCumulative) - BigInt(currentReceipt.spent);
|
|
130
|
+
if (available < tickCost)
|
|
131
|
+
return paymentRequired(available);
|
|
129
132
|
const chargedReceipt = {
|
|
130
133
|
...currentReceipt,
|
|
131
134
|
spent: (BigInt(currentReceipt.spent) + tickCost).toString(),
|
|
@@ -150,7 +153,9 @@ export function sse(options) {
|
|
|
150
153
|
// For null-body statuses, the request shape determines whether the
|
|
151
154
|
// response is management (no charge) or plain content (charge one tick).
|
|
152
155
|
if (isNullBodyStatus(chargedResponse.status)) {
|
|
153
|
-
|
|
156
|
+
const result = await chargePlainResponse();
|
|
157
|
+
if (!result.ok)
|
|
158
|
+
return paymentRequired(result.channel.highestVoucherAmount - result.channel.spent);
|
|
154
159
|
return chargedResponse;
|
|
155
160
|
}
|
|
156
161
|
const stream = new ReadableStream({
|
|
@@ -193,20 +198,32 @@ export function sse(options) {
|
|
|
193
198
|
/** Default SSE serve: iterates values and emits `event: message` per value. */
|
|
194
199
|
export function defaultServe(options) {
|
|
195
200
|
const iterable = typeof options.generate === 'function' ? options.generate() : options.generate;
|
|
201
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
196
202
|
const encoder = new TextEncoder();
|
|
203
|
+
let closed = false;
|
|
197
204
|
const stream = new ReadableStream({
|
|
198
|
-
async
|
|
205
|
+
async pull(controller) {
|
|
206
|
+
if (closed)
|
|
207
|
+
return;
|
|
199
208
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
const next = await iterator.next();
|
|
210
|
+
if (next.done) {
|
|
211
|
+
closed = true;
|
|
212
|
+
controller.close();
|
|
213
|
+
return;
|
|
202
214
|
}
|
|
215
|
+
controller.enqueue(encoder.encode(Sse_core.formatMessageEvent(next.value)));
|
|
203
216
|
}
|
|
204
217
|
catch (e) {
|
|
218
|
+
closed = true;
|
|
205
219
|
controller.error(e);
|
|
206
220
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
},
|
|
222
|
+
cancel() {
|
|
223
|
+
closed = true;
|
|
224
|
+
void Promise.resolve()
|
|
225
|
+
.then(() => iterator.return?.())
|
|
226
|
+
.catch(() => { });
|
|
210
227
|
},
|
|
211
228
|
});
|
|
212
229
|
return new Response(stream, {
|
|
@@ -114,12 +114,17 @@ export async function createVoucherPayload(client, account, descriptor, cumulati
|
|
|
114
114
|
*/
|
|
115
115
|
export async function createClosePayload(client, account, descriptor, cumulativeAmount, chainId, escrow = tip20ChannelEscrow) {
|
|
116
116
|
const voucher = await createVoucherPayload(client, account, descriptor, cumulativeAmount, chainId, escrow);
|
|
117
|
+
const closeSignature = await Voucher.signCloseAuthorization(client, account, {
|
|
118
|
+
channelId: voucher.channelId,
|
|
119
|
+
cumulativeAmount: uint96(BigInt(voucher.cumulativeAmount)),
|
|
120
|
+
}, escrow, chainId);
|
|
117
121
|
return {
|
|
118
122
|
action: 'close',
|
|
119
123
|
channelId: voucher.channelId,
|
|
120
124
|
descriptor,
|
|
121
125
|
cumulativeAmount: voucher.cumulativeAmount,
|
|
122
126
|
signature: voucher.signature,
|
|
127
|
+
closeSignature,
|
|
123
128
|
};
|
|
124
129
|
}
|
|
125
130
|
/**
|
|
@@ -45,6 +45,7 @@ export declare function session(parameters?: session.Parameters): Method.Client<
|
|
|
45
45
|
}, import("zod/v4/core").$strip>, import("zod/mini").ZodMiniObject<{
|
|
46
46
|
action: import("zod/mini").ZodMiniLiteral<"close">;
|
|
47
47
|
channelId: import("zod/mini").ZodMiniString<string>;
|
|
48
|
+
closeSignature: import("zod/mini").ZodMiniString<string>;
|
|
48
49
|
cumulativeAmount: import("zod/mini").ZodMiniString<string>;
|
|
49
50
|
descriptor: import("zod/mini").ZodMiniOptional<import("zod/mini").ZodMiniCustom<Channel.ChannelDescriptor, Channel.ChannelDescriptor>>;
|
|
50
51
|
signature: import("zod/mini").ZodMiniString<string>;
|
|
@@ -195,7 +195,7 @@ export function session(parameters = {}) {
|
|
|
195
195
|
}
|
|
196
196
|
},
|
|
197
197
|
});
|
|
198
|
-
const topUpChannelIfNeeded = async ({ challenge, channel, deposit, fetch, input, requiredCumulative, }) => {
|
|
198
|
+
const topUpChannelIfNeeded = async ({ challenge, channel, deposit, fetch, input, requiredCumulative, signal, }) => {
|
|
199
199
|
const knownDeposit = channel.deposit > deposit ? channel.deposit : deposit;
|
|
200
200
|
const additionalDeposit = resolveAutomaticTopUp({
|
|
201
201
|
deposit: knownDeposit,
|
|
@@ -215,6 +215,7 @@ export function session(parameters = {}) {
|
|
|
215
215
|
createSessionCredential: (challenge, context) => method.createCredential({ challenge, context }),
|
|
216
216
|
fetch,
|
|
217
217
|
input,
|
|
218
|
+
signal,
|
|
218
219
|
});
|
|
219
220
|
const nextDeposit = knownDeposit + additionalDeposit;
|
|
220
221
|
if (nextDeposit === channel.deposit)
|
|
@@ -228,7 +229,7 @@ export function session(parameters = {}) {
|
|
|
228
229
|
await store.set(updated);
|
|
229
230
|
sink.notifyUpdate(updated);
|
|
230
231
|
};
|
|
231
|
-
MethodChallenge.register(method, async ({ challenge, context, fetch, input }) => {
|
|
232
|
+
MethodChallenge.register(method, async ({ challenge, context, fetch, input, signal }) => {
|
|
232
233
|
if (!isTip1034SessionChallenge(challenge))
|
|
233
234
|
return;
|
|
234
235
|
const sessionContext = context === undefined ? undefined : sessionContextSchema.parse(context);
|
|
@@ -264,6 +265,7 @@ export function session(parameters = {}) {
|
|
|
264
265
|
fetch,
|
|
265
266
|
input,
|
|
266
267
|
requiredCumulative,
|
|
268
|
+
signal,
|
|
267
269
|
});
|
|
268
270
|
});
|
|
269
271
|
return MethodResponse.register(method, async ({ challenge, credential, fetch, headers, input, refetch, response, signal }) => {
|
|
@@ -289,7 +291,7 @@ export function session(parameters = {}) {
|
|
|
289
291
|
createSessionCredential: (challenge, context) => method.createCredential({ challenge, context }),
|
|
290
292
|
fetch,
|
|
291
293
|
getChannel: () => channel ?? null,
|
|
292
|
-
async topUpIfNeeded({ deposit, requiredCumulative }) {
|
|
294
|
+
async topUpIfNeeded({ deposit, requiredCumulative, signal: topUpSignal }) {
|
|
293
295
|
if (!channel)
|
|
294
296
|
return;
|
|
295
297
|
await topUpChannelIfNeeded({
|
|
@@ -299,11 +301,12 @@ export function session(parameters = {}) {
|
|
|
299
301
|
fetch,
|
|
300
302
|
input,
|
|
301
303
|
requiredCumulative,
|
|
304
|
+
signal: topUpSignal,
|
|
302
305
|
});
|
|
303
306
|
},
|
|
304
307
|
};
|
|
305
308
|
return wrapSseResponse({
|
|
306
|
-
onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
|
|
309
|
+
onNeedVoucher: (event, streamSignal) => handleSseNeedVoucher({ challenge, driver, input, signal: streamSignal }, event),
|
|
307
310
|
onReceipt() { },
|
|
308
311
|
response,
|
|
309
312
|
signal,
|
|
@@ -235,6 +235,7 @@ export function sessionManager(parameters) {
|
|
|
235
235
|
allowedChainIds: parameters.allowedChainIds,
|
|
236
236
|
account: parameters.account,
|
|
237
237
|
getClient: parameters.client ? () => parameters.client : parameters.getClient,
|
|
238
|
+
resolveAccount: parameters.resolveAccount,
|
|
238
239
|
});
|
|
239
240
|
const wrappedFetch = Fetch.from({
|
|
240
241
|
fetch: config.fetch,
|
|
@@ -486,6 +487,7 @@ export function sessionManager(parameters) {
|
|
|
486
487
|
channelId: parameters.channelId,
|
|
487
488
|
additionalDeposit,
|
|
488
489
|
knownDeposit: deposit,
|
|
490
|
+
signal: parameters.signal,
|
|
489
491
|
});
|
|
490
492
|
}
|
|
491
493
|
async function restoreCumulative(channelId, cumulativeAmount) {
|
|
@@ -700,6 +702,7 @@ export function sessionManager(parameters) {
|
|
|
700
702
|
context,
|
|
701
703
|
fetch: config.fetch,
|
|
702
704
|
input: probeUrl,
|
|
705
|
+
signal: init?.signal,
|
|
703
706
|
});
|
|
704
707
|
return createSessionCredential(challenge, context);
|
|
705
708
|
},
|
|
@@ -80,6 +80,8 @@ export type TopUpRequirement = {
|
|
|
80
80
|
input: RequestInfo | URL;
|
|
81
81
|
/** Minimum cumulative voucher amount the server needs. */
|
|
82
82
|
requiredCumulative: bigint;
|
|
83
|
+
/** Cancels nested management requests for this top-up. */
|
|
84
|
+
signal?: AbortSignal | undefined;
|
|
83
85
|
};
|
|
84
86
|
/** Inputs needed to validate a caller-requested manager top-up. */
|
|
85
87
|
export type ResolveManualTopUpParameters = {
|
|
@@ -156,6 +158,8 @@ export type ResolveNeedVoucherContextParameters = {
|
|
|
156
158
|
getChannel(): ChannelEntry | null;
|
|
157
159
|
/** Original paid resource URL; converted to management URL by the caller. */
|
|
158
160
|
input: RequestInfo | URL;
|
|
161
|
+
/** Cancels top-up work triggered by this event. */
|
|
162
|
+
signal?: AbortSignal | undefined;
|
|
159
163
|
/** Performs the deposit top-up when server-required cumulative exceeds deposit. */
|
|
160
164
|
topUpIfNeeded(parameters: TopUpRequirement): Promise<void>;
|
|
161
165
|
};
|
|
@@ -207,6 +211,8 @@ export type PostTopUpParameters = {
|
|
|
207
211
|
fetch: typeof globalThis.fetch;
|
|
208
212
|
/** Original paid resource URL; normalized to a management URL before posting. */
|
|
209
213
|
input: RequestInfo | URL;
|
|
214
|
+
/** Cancels the top-up management request. */
|
|
215
|
+
signal?: AbortSignal | undefined;
|
|
210
216
|
};
|
|
211
217
|
/** Inputs for retrying an HTTP 402 with a top-up/voucher management round trip. */
|
|
212
218
|
export type RetryHttpPaymentRequiredParameters = {
|
|
@@ -334,7 +340,7 @@ export type SseResponseFrame = {
|
|
|
334
340
|
/** Inputs for driving an open paid SSE response. */
|
|
335
341
|
export type DriveSseResponseParameters = {
|
|
336
342
|
/** Handles a request for the stream's next cumulative voucher. */
|
|
337
|
-
onNeedVoucher(event: NeedVoucherEvent): Promise<void>;
|
|
343
|
+
onNeedVoucher(event: NeedVoucherEvent, signal?: AbortSignal | undefined): Promise<void>;
|
|
338
344
|
/** Handles a payment receipt emitted by the stream. */
|
|
339
345
|
onReceipt(receipt: SessionReceipt): void;
|
|
340
346
|
/** Open SSE response to drive. */
|
|
@@ -351,6 +357,7 @@ export declare function handleSseNeedVoucher(parameters: {
|
|
|
351
357
|
challenge: TempoSessionChallenge | null;
|
|
352
358
|
driver: SsePaymentDriver;
|
|
353
359
|
input: RequestInfo | URL;
|
|
360
|
+
signal?: AbortSignal | undefined;
|
|
354
361
|
}, event: NeedVoucherEvent): Promise<void>;
|
|
355
362
|
/** Options accepted by the auto-driving WebSocket session flow. */
|
|
356
363
|
export type WebSocketDriverOptions = {
|
|
@@ -214,6 +214,7 @@ export async function resolveNeedVoucherContext(parameters) {
|
|
|
214
214
|
channelId: parameters.expectedChannelId,
|
|
215
215
|
deposit: eventAmounts.deposit,
|
|
216
216
|
requiredCumulative: eventAmounts.requiredCumulative,
|
|
217
|
+
...(parameters.signal ? { signal: parameters.signal } : {}),
|
|
217
218
|
});
|
|
218
219
|
const channel = parameters.getChannel();
|
|
219
220
|
if (!channel || channel.channelId !== parameters.expectedChannelId) {
|
|
@@ -314,6 +315,7 @@ export async function postTopUp(parameters) {
|
|
|
314
315
|
createSessionCredential: parameters.createSessionCredential,
|
|
315
316
|
fetch: parameters.fetch,
|
|
316
317
|
input: parameters.input,
|
|
318
|
+
signal: parameters.signal,
|
|
317
319
|
});
|
|
318
320
|
if (!response.ok)
|
|
319
321
|
throw new Error(`Top-up POST failed with status ${response.status}`);
|
|
@@ -324,6 +326,7 @@ export async function postTopUp(parameters) {
|
|
|
324
326
|
async function postManagementCredential(parameters) {
|
|
325
327
|
const post = async (challenge) => parameters.fetch(managementInput(parameters.input), {
|
|
326
328
|
method: 'POST',
|
|
329
|
+
...(parameters.signal ? { signal: parameters.signal } : {}),
|
|
327
330
|
headers: {
|
|
328
331
|
[Constants.Headers.authorization]: await parameters.createSessionCredential(challenge, parameters.context),
|
|
329
332
|
},
|
|
@@ -352,6 +355,8 @@ export async function retryHttpPaymentRequired(parameters) {
|
|
|
352
355
|
channelId: snapshot.channelId,
|
|
353
356
|
deposit: BigInt(snapshot.deposit),
|
|
354
357
|
requiredCumulative,
|
|
358
|
+
signal: parameters.init?.signal ??
|
|
359
|
+
(parameters.input instanceof Request ? parameters.input.signal : undefined),
|
|
355
360
|
});
|
|
356
361
|
const currentChannel = parameters.getChannel();
|
|
357
362
|
if (!currentChannel?.descriptor || currentChannel.channelId !== snapshot.channelId) {
|
|
@@ -453,7 +458,8 @@ export async function closeHttpSession(parameters) {
|
|
|
453
458
|
* credential creation, and top-up policy stay in `SessionManager`.
|
|
454
459
|
*/
|
|
455
460
|
export async function openSseSession(input, init, driver) {
|
|
456
|
-
const { onReceipt, signal, ...fetchInit } = init ?? {};
|
|
461
|
+
const { onReceipt, signal: explicitSignal, ...fetchInit } = init ?? {};
|
|
462
|
+
const signal = explicitSignal ?? (input instanceof Request ? input.signal : undefined);
|
|
457
463
|
const sseInit = {
|
|
458
464
|
...fetchInit,
|
|
459
465
|
headers: {
|
|
@@ -475,7 +481,7 @@ export function consumeSseSessionResponse(input, response, options, driver) {
|
|
|
475
481
|
throw new Error('Response has no body.');
|
|
476
482
|
const challenge = driver.getChallenge();
|
|
477
483
|
return iterateSseMessages({
|
|
478
|
-
onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input }, event),
|
|
484
|
+
onNeedVoucher: (event) => handleSseNeedVoucher({ challenge, driver, input, signal: options?.signal }, event),
|
|
479
485
|
onReceipt(receipt) {
|
|
480
486
|
driver.acceptReceipt(receipt);
|
|
481
487
|
options?.onReceipt?.(receipt);
|
|
@@ -535,7 +541,7 @@ export async function* driveSseResponse(parameters) {
|
|
|
535
541
|
yield { data: event.data, raw };
|
|
536
542
|
break;
|
|
537
543
|
case 'payment-need-voucher':
|
|
538
|
-
await parameters.onNeedVoucher(event.data);
|
|
544
|
+
await parameters.onNeedVoucher(event.data, parameters.signal);
|
|
539
545
|
break;
|
|
540
546
|
case 'payment-receipt':
|
|
541
547
|
parameters.onReceipt(event.data);
|
|
@@ -632,6 +638,7 @@ export async function handleSseNeedVoucher(parameters, event) {
|
|
|
632
638
|
expectedChannelId: channel.channelId,
|
|
633
639
|
getChannel: parameters.driver.getChannel,
|
|
634
640
|
input: parameters.input,
|
|
641
|
+
signal: parameters.signal,
|
|
635
642
|
topUpIfNeeded: parameters.driver.topUpIfNeeded,
|
|
636
643
|
});
|
|
637
644
|
if (resolution.status !== 'ready')
|
|
@@ -642,6 +649,7 @@ export async function handleSseNeedVoucher(parameters, event) {
|
|
|
642
649
|
createSessionCredential: parameters.driver.createSessionCredential,
|
|
643
650
|
fetch: parameters.driver.fetch,
|
|
644
651
|
input: parameters.input,
|
|
652
|
+
signal: parameters.signal,
|
|
645
653
|
});
|
|
646
654
|
if (!voucherResponse.ok) {
|
|
647
655
|
throw new Error(`Voucher POST failed with status ${voucherResponse.status}`);
|
|
@@ -868,6 +876,7 @@ async function handleNeedVoucher(parameters, event) {
|
|
|
868
876
|
expectedChannelId: parameters.socketState.channelId,
|
|
869
877
|
getChannel: parameters.driver.getChannel,
|
|
870
878
|
input: parameters.driver.httpUrl,
|
|
879
|
+
signal: parameters.driver.options?.signal,
|
|
871
880
|
topUpIfNeeded: parameters.driver.topUpIfNeeded,
|
|
872
881
|
});
|
|
873
882
|
if (resolution.status === 'ignored') {
|
|
@@ -292,8 +292,23 @@ export async function simulateCredentialTransaction(parameters) {
|
|
|
292
292
|
}
|
|
293
293
|
function validateCredentialSponsorship(parameters) {
|
|
294
294
|
const { allowedFeeTokens, challengeExpires, chainId, details, feePayer, feePayerPolicy, transaction, } = parameters;
|
|
295
|
-
if (!feePayer
|
|
295
|
+
if (!feePayer)
|
|
296
296
|
return;
|
|
297
|
+
if (feePayer === true) {
|
|
298
|
+
assertPrecompileFeePayerPolicy({ prepared: transaction, policy: feePayerPolicy });
|
|
299
|
+
if (feePayerPolicy?.allowKeyAuthorization === false &&
|
|
300
|
+
transaction.keyAuthorization !== undefined)
|
|
301
|
+
throw new BadRequestError({ reason: 'fee-payer policy keyAuthorization is not allowed' });
|
|
302
|
+
FeePayer.assertAllowedFeeToken(transaction, allowedFeeTokens);
|
|
303
|
+
FeePayer.assertTransactionPolicy({
|
|
304
|
+
challengeExpires,
|
|
305
|
+
chainId,
|
|
306
|
+
details,
|
|
307
|
+
policy: feePayerPolicy,
|
|
308
|
+
transaction,
|
|
309
|
+
});
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
297
312
|
FeePayer.prepareSponsoredTransaction({
|
|
298
313
|
account: feePayer,
|
|
299
314
|
allowedFeeTokens,
|
|
@@ -106,6 +106,8 @@ export type CloseCredentialPayload = {
|
|
|
106
106
|
cumulativeAmount: RawAmountString;
|
|
107
107
|
/** Voucher signature for `cumulativeAmount`. */
|
|
108
108
|
signature: Hex;
|
|
109
|
+
/** Separate signature authorizing the cooperative close action. */
|
|
110
|
+
closeSignature: Hex;
|
|
109
111
|
};
|
|
110
112
|
/**
|
|
111
113
|
* TIP20EscrowChannel precompile session credential payload (discriminated union).
|
|
@@ -22,10 +22,26 @@ export declare const voucherTypes: {
|
|
|
22
22
|
readonly type: 'uint96';
|
|
23
23
|
}];
|
|
24
24
|
};
|
|
25
|
+
/** EIP-712 type that distinguishes cooperative close authorization from a voucher. */
|
|
26
|
+
export declare const closeAuthorizationTypes: {
|
|
27
|
+
readonly CloseAuthorization: readonly [{
|
|
28
|
+
readonly name: 'channelId';
|
|
29
|
+
readonly type: 'bytes32';
|
|
30
|
+
}, {
|
|
31
|
+
readonly name: 'cumulativeAmount';
|
|
32
|
+
readonly type: 'uint96';
|
|
33
|
+
}];
|
|
34
|
+
};
|
|
25
35
|
/**
|
|
26
36
|
* Sign a voucher with an account.
|
|
27
37
|
*/
|
|
28
38
|
export declare function signVoucher(client: Client, account: Account, voucher: Voucher, verifyingContract: Address, chainId: number): Promise<Hex>;
|
|
39
|
+
/**
|
|
40
|
+
* Signs a distinct EIP-712 authorization for cooperatively closing a channel.
|
|
41
|
+
*/
|
|
42
|
+
export declare function signCloseAuthorization(client: Client, account: Account, authorization: Voucher, verifyingContract: Address, chainId: number): Promise<Hex>;
|
|
43
|
+
/** Verifies that a cooperative-close signature belongs to a channel authority. */
|
|
44
|
+
export declare function verifyCloseAuthorization(escrowContract: Address, chainId: number, authorization: SignedVoucher, expectedSigners: readonly Address[]): boolean;
|
|
29
45
|
/**
|
|
30
46
|
* Verify a voucher signature matches the expected signer.
|
|
31
47
|
*
|
|
@@ -29,6 +29,21 @@ export const voucherTypes = {
|
|
|
29
29
|
{ name: 'cumulativeAmount', type: 'uint96' },
|
|
30
30
|
],
|
|
31
31
|
};
|
|
32
|
+
/** EIP-712 type that distinguishes cooperative close authorization from a voucher. */
|
|
33
|
+
export const closeAuthorizationTypes = {
|
|
34
|
+
CloseAuthorization: [
|
|
35
|
+
{ name: 'channelId', type: 'bytes32' },
|
|
36
|
+
{ name: 'cumulativeAmount', type: 'uint96' },
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
function getCloseAuthorizationPayload(verifyingContract, chainId, authorization) {
|
|
40
|
+
return hashTypedData({
|
|
41
|
+
domain: getVoucherDomain(verifyingContract, chainId),
|
|
42
|
+
types: closeAuthorizationTypes,
|
|
43
|
+
primaryType: 'CloseAuthorization',
|
|
44
|
+
message: authorization,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
32
47
|
function getVoucherDigest(chainId, voucher) {
|
|
33
48
|
return Channel.getVoucherSignPayload({
|
|
34
49
|
chainId,
|
|
@@ -90,6 +105,39 @@ export async function signVoucher(client, account, voucher, verifyingContract, c
|
|
|
90
105
|
throw new Error(`TIP-1034 vouchers require a TIP-1020 primitive signature; received "${envelope.type}".`);
|
|
91
106
|
return SignatureEnvelope.serialize(envelope);
|
|
92
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Signs a distinct EIP-712 authorization for cooperatively closing a channel.
|
|
110
|
+
*/
|
|
111
|
+
export async function signCloseAuthorization(client, account, authorization, verifyingContract, chainId) {
|
|
112
|
+
const signature = await signTypedData(client, {
|
|
113
|
+
account,
|
|
114
|
+
domain: getVoucherDomain(verifyingContract, chainId),
|
|
115
|
+
types: closeAuthorizationTypes,
|
|
116
|
+
primaryType: 'CloseAuthorization',
|
|
117
|
+
message: authorization,
|
|
118
|
+
});
|
|
119
|
+
const envelope = SignatureEnvelope.from(signature);
|
|
120
|
+
if (!isPrimitiveEnvelope(envelope.type))
|
|
121
|
+
throw new Error(`TIP-1034 close authorizations require a TIP-1020 primitive signature; received "${envelope.type}".`);
|
|
122
|
+
return SignatureEnvelope.serialize(envelope);
|
|
123
|
+
}
|
|
124
|
+
/** Verifies that a cooperative-close signature belongs to a channel authority. */
|
|
125
|
+
export function verifyCloseAuthorization(escrowContract, chainId, authorization, expectedSigners) {
|
|
126
|
+
try {
|
|
127
|
+
const envelope = SignatureEnvelope.from(authorization.signature);
|
|
128
|
+
if (!isPrimitiveEnvelope(envelope.type))
|
|
129
|
+
return false;
|
|
130
|
+
if (SignatureEnvelope.serialize(envelope).toLowerCase() !== authorization.signature.toLowerCase())
|
|
131
|
+
return false;
|
|
132
|
+
const payload = getCloseAuthorizationPayload(escrowContract, chainId, authorization);
|
|
133
|
+
const signer = SignatureEnvelope.extractAddress({ payload, signature: envelope });
|
|
134
|
+
const valid = SignatureEnvelope.verify(envelope, { address: signer, payload });
|
|
135
|
+
return valid && expectedSigners.some((expected) => TempoAddress.isEqual(signer, expected));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
93
141
|
/**
|
|
94
142
|
* Verify a voucher signature matches the expected signer.
|
|
95
143
|
*
|
|
@@ -189,6 +189,8 @@ export type VerifyAndAcceptVoucherParameters = {
|
|
|
189
189
|
methodDetails: SessionMethodDetails;
|
|
190
190
|
/** Minimum allowed voucher delta in raw units. */
|
|
191
191
|
minVoucherDelta: bigint;
|
|
192
|
+
/** Whether this presentation must advance the accepted voucher. */
|
|
193
|
+
requireAdvance?: boolean | undefined;
|
|
192
194
|
/** Server channel store. */
|
|
193
195
|
store: ChannelStore;
|
|
194
196
|
/** Signed cumulative voucher to verify and accept. */
|
|
@@ -255,6 +257,14 @@ export interface BaseState {
|
|
|
255
257
|
token: Address;
|
|
256
258
|
/** Number of charge operations (API requests) fulfilled in the current session. */
|
|
257
259
|
units: number;
|
|
260
|
+
/** Uncommitted stream charges shared across workers. */
|
|
261
|
+
streamReservations?: Record<string, {
|
|
262
|
+
amount: bigint;
|
|
263
|
+
units: number;
|
|
264
|
+
expiresAt: number;
|
|
265
|
+
}> | undefined;
|
|
266
|
+
/** Active cross-worker claim for server-scheduled settlement. */
|
|
267
|
+
scheduledSettlementLease?: ScheduledSettlementLease | undefined;
|
|
258
268
|
/** ISO 8601 timestamp of the last server-scheduled settlement. */
|
|
259
269
|
lastSettlementAt?: string | undefined;
|
|
260
270
|
/** Cumulative spent value when the last server-scheduled settlement ran. */
|
|
@@ -262,6 +272,11 @@ export interface BaseState {
|
|
|
262
272
|
/** Charge operation count when the last server-scheduled settlement ran. */
|
|
263
273
|
lastSettlementUnits?: number | undefined;
|
|
264
274
|
}
|
|
275
|
+
/** Expiring ownership claim for one server-scheduled settlement attempt. */
|
|
276
|
+
export type ScheduledSettlementLease = {
|
|
277
|
+
expiresAt: number;
|
|
278
|
+
owner: string;
|
|
279
|
+
};
|
|
265
280
|
/** Returns whether a channel is backed by the TIP20EscrowChannel precompile. */
|
|
266
281
|
export declare function isPrecompileState(state: State): state is BaseState & PrecompileBackendState;
|
|
267
282
|
/** Returns the greater bigint value without decreasing persisted monotonic state. */
|
|
@@ -315,6 +330,8 @@ export declare function verifyAndAcceptVoucher(parameters: VerifyAndAcceptVouche
|
|
|
315
330
|
* - **D1 / SQL**: Database transactions
|
|
316
331
|
*/
|
|
317
332
|
export type ChannelStore = {
|
|
333
|
+
/** False when the backing store only serializes mutations within this process. */
|
|
334
|
+
atomic?: boolean | undefined;
|
|
318
335
|
getChannel(channelId: Hex): Promise<State | null>;
|
|
319
336
|
/**
|
|
320
337
|
* Atomic read-modify-write for channel state.
|
|
@@ -327,11 +344,12 @@ export type ChannelStore = {
|
|
|
327
344
|
* Returns a `Promise` that resolves once `updateChannel` is called for
|
|
328
345
|
* `channelId`. Implementations should resolve immediately if the channel
|
|
329
346
|
* was updated between the call to `waitForUpdate` and the `Promise`
|
|
330
|
-
* being awaited.
|
|
347
|
+
* being awaited. Implementations must reject and release the waiter when
|
|
348
|
+
* `signal` aborts.
|
|
331
349
|
*
|
|
332
350
|
* When not implemented, callers fall back to polling.
|
|
333
351
|
*/
|
|
334
|
-
waitForUpdate?(channelId: Hex): Promise<void>;
|
|
352
|
+
waitForUpdate?(channelId: Hex, signal?: AbortSignal): Promise<void>;
|
|
335
353
|
/**
|
|
336
354
|
* Atomic read-modify-write that returns the callback's `result` directly.
|
|
337
355
|
*
|
|
@@ -353,4 +371,6 @@ export declare function fromStore(store: Store.Store | Store.AtomicStore): Chann
|
|
|
353
371
|
* when the channel exists but cannot currently be charged.
|
|
354
372
|
*/
|
|
355
373
|
export declare function deductFromChannel(store: ChannelStore, channelId: State['channelId'], amount: bigint): Promise<DeductResult>;
|
|
374
|
+
/** Returns unexpired stream reservations that ordinary deductions must respect. */
|
|
375
|
+
export declare function reservedStreamAmount(state: Pick<State, 'streamReservations'>): bigint;
|
|
356
376
|
//# sourceMappingURL=ChannelStore.d.ts.map
|
|
@@ -224,9 +224,11 @@ export async function validateVoucher(parameters) {
|
|
|
224
224
|
}
|
|
225
225
|
/** Verifies a cumulative voucher and returns a session receipt after store reconciliation. */
|
|
226
226
|
export async function verifyAndAcceptVoucher(parameters) {
|
|
227
|
-
const { store, challenge, channel, voucher, channelState } = parameters;
|
|
227
|
+
const { store, challenge, channel, voucher, channelState, requireAdvance } = parameters;
|
|
228
228
|
const validation = await validateVoucher(parameters);
|
|
229
|
-
if (validation.alreadyAccepted)
|
|
229
|
+
if (validation.alreadyAccepted) {
|
|
230
|
+
if (requireAdvance)
|
|
231
|
+
throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
|
|
230
232
|
return createSessionReceipt({
|
|
231
233
|
challengeId: challenge.id,
|
|
232
234
|
channelId: voucher.channelId,
|
|
@@ -234,7 +236,14 @@ export async function verifyAndAcceptVoucher(parameters) {
|
|
|
234
236
|
spent: channel.spent,
|
|
235
237
|
units: channel.units,
|
|
236
238
|
});
|
|
237
|
-
|
|
239
|
+
}
|
|
240
|
+
const updated = await store.updateChannel(voucher.channelId, (current) => {
|
|
241
|
+
if (requireAdvance &&
|
|
242
|
+
current !== null &&
|
|
243
|
+
voucher.cumulativeAmount <= current.highestVoucherAmount)
|
|
244
|
+
throw new DeltaTooSmallError({ reason: 'voucher does not add new funds for this request' });
|
|
245
|
+
return acceptVoucherStateUpdate({ channelState, current, voucher });
|
|
246
|
+
});
|
|
238
247
|
if (!updated)
|
|
239
248
|
throw new ChannelNotFoundError({ reason: 'channel not found' });
|
|
240
249
|
return createSessionReceipt({
|
|
@@ -348,6 +357,7 @@ export function fromStore(store) {
|
|
|
348
357
|
}
|
|
349
358
|
}
|
|
350
359
|
const cs = {
|
|
360
|
+
atomic: typeof atomicUpdate === 'function',
|
|
351
361
|
async getChannel(channelId) {
|
|
352
362
|
const normalizedChannelId = normalizeChannelId(channelId);
|
|
353
363
|
return normalizeMaybeState(normalizedChannelId, await stateStore.get(normalizedChannelId));
|
|
@@ -355,15 +365,30 @@ export function fromStore(store) {
|
|
|
355
365
|
async updateChannel(channelId, fn) {
|
|
356
366
|
return update(channelId, fn);
|
|
357
367
|
},
|
|
358
|
-
waitForUpdate(channelId) {
|
|
359
|
-
return new Promise((resolve) => {
|
|
368
|
+
waitForUpdate(channelId, signal) {
|
|
369
|
+
return new Promise((resolve, reject) => {
|
|
360
370
|
const normalizedChannelId = normalizeChannelId(channelId);
|
|
371
|
+
if (signal?.aborted) {
|
|
372
|
+
reject(signal.reason ?? new Error('aborted'));
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
361
375
|
let set = runtime.waiters.get(normalizedChannelId);
|
|
362
376
|
if (!set) {
|
|
363
377
|
set = new Set();
|
|
364
378
|
runtime.waiters.set(normalizedChannelId, set);
|
|
365
379
|
}
|
|
366
|
-
|
|
380
|
+
const settle = () => {
|
|
381
|
+
signal?.removeEventListener('abort', onAbort);
|
|
382
|
+
resolve();
|
|
383
|
+
};
|
|
384
|
+
const onAbort = () => {
|
|
385
|
+
set?.delete(settle);
|
|
386
|
+
if (set?.size === 0)
|
|
387
|
+
runtime.waiters.delete(normalizedChannelId);
|
|
388
|
+
reject(signal?.reason ?? new Error('aborted'));
|
|
389
|
+
};
|
|
390
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
391
|
+
set.add(settle);
|
|
367
392
|
});
|
|
368
393
|
},
|
|
369
394
|
};
|
|
@@ -404,9 +429,14 @@ function planDeduction(current, amount) {
|
|
|
404
429
|
return { op: 'noop', result: { ok: false, channel: current } };
|
|
405
430
|
if (current.closeRequestedAt !== 0n)
|
|
406
431
|
return { op: 'noop', result: { ok: false, channel: current } };
|
|
407
|
-
if (current.highestVoucherAmount - current.spent < amount)
|
|
432
|
+
if (current.highestVoucherAmount - current.spent - reservedStreamAmount(current) < amount)
|
|
408
433
|
return { op: 'noop', result: { ok: false, channel: current } };
|
|
409
434
|
const next = { ...current, spent: current.spent + amount, units: current.units + 1 };
|
|
410
435
|
return { op: 'set', value: next, result: { ok: true, channel: next } };
|
|
411
436
|
}
|
|
437
|
+
/** Returns unexpired stream reservations that ordinary deductions must respect. */
|
|
438
|
+
export function reservedStreamAmount(state) {
|
|
439
|
+
const now = Date.now();
|
|
440
|
+
return Object.values(state.streamReservations ?? {}).reduce((total, reservation) => total + (reservation.expiresAt > now ? reservation.amount : 0n), 0n);
|
|
441
|
+
}
|
|
412
442
|
//# sourceMappingURL=ChannelStore.js.map
|
|
@@ -79,6 +79,8 @@ export type BroadcastCredentialPayloadParameters = {
|
|
|
79
79
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
80
80
|
/** Discriminated session credential payload to verify. */
|
|
81
81
|
payload: SessionCredentialPayload;
|
|
82
|
+
/** Whether an open or voucher credential must add new funds for this request. */
|
|
83
|
+
requireVoucherAdvance?: boolean | undefined;
|
|
82
84
|
/** Server-side channel store. */
|
|
83
85
|
store: ChannelStore.ChannelStore;
|
|
84
86
|
};
|
|
@@ -102,7 +104,10 @@ export type VoucherCredentialActionParameters = BroadcastCredentialActionParamet
|
|
|
102
104
|
/** Inputs for broadcasting and settling a cooperative close credential. */
|
|
103
105
|
export type CloseCredentialActionParameters = BroadcastCredentialActionParameters<'close'>;
|
|
104
106
|
/** Inputs for validating a session credential without applying its state transition. */
|
|
105
|
-
export type ValidateCredentialPayloadParameters = Pick<BroadcastCredentialPayloadParameters, 'account' | 'channelStateTtl' | 'chainId' | 'client' | 'credentialSource' | 'escrow' | 'expectedOperator' | 'feePayer' | 'feePayerPolicy' | 'feeToken' | 'lastOnChainVerified' | 'minVoucherDelta' | 'payload' | 'store' | 'challenge'
|
|
107
|
+
export type ValidateCredentialPayloadParameters = Pick<BroadcastCredentialPayloadParameters, 'account' | 'channelStateTtl' | 'chainId' | 'client' | 'credentialSource' | 'escrow' | 'expectedOperator' | 'feePayer' | 'feePayerPolicy' | 'feeToken' | 'lastOnChainVerified' | 'minVoucherDelta' | 'payload' | 'store' | 'challenge'> & {
|
|
108
|
+
/** Allows terminal broadcast to recover accepted funds before rejecting a pending close. */
|
|
109
|
+
allowPendingClose?: boolean | undefined;
|
|
110
|
+
};
|
|
106
111
|
/** Non-mutating result produced by session credential validation. */
|
|
107
112
|
export type CredentialPayloadValidation = {
|
|
108
113
|
/** Session action validated from the credential. */
|