mppx 0.13.5 → 0.14.0
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 +41 -0
- package/dist/Credential.d.ts +3 -0
- package/dist/Credential.js +9 -0
- package/dist/cli/cli.js +131 -131
- package/dist/cli/plugins/evm.js +2 -2
- package/dist/cli/plugins/index.js +11 -11
- package/dist/client/internal/MethodChallenge.d.ts +2 -0
- package/dist/client/internal/MethodChallenge.js +4 -0
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/server/Mppx.js +31 -5
- package/dist/stripe/server/Methods.js +4 -1
- package/dist/tempo/client/Charge.js +1 -0
- package/dist/tempo/internal/auto-swap.d.ts +10 -0
- package/dist/tempo/internal/auto-swap.js +35 -8
- package/dist/tempo/internal/fee-payer.d.ts +8 -0
- package/dist/tempo/internal/fee-payer.js +20 -12
- package/dist/tempo/legacy/session/Chain.js +8 -2
- package/dist/tempo/server/Methods.d.ts +7 -2
- package/dist/tempo/server/Methods.js +20 -2
- package/dist/tempo/server/Relay.js +6 -22
- package/dist/tempo/server/Subscription.js +3 -0
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/transport.d.ts +3 -0
- package/dist/tempo/server/internal/transport.js +5 -1
- package/dist/tempo/session/client/ChannelOps.d.ts +2 -0
- package/dist/tempo/session/client/ChannelStore.d.ts +12 -1
- package/dist/tempo/session/client/ChannelStore.js +47 -0
- package/dist/tempo/session/client/CredentialState.js +8 -3
- package/dist/tempo/session/client/Session.js +50 -33
- package/dist/tempo/session/client/SessionManager.js +186 -65
- package/dist/tempo/session/client/Transports.d.ts +6 -0
- package/dist/tempo/session/client/Transports.js +52 -7
- package/dist/tempo/session/precompile/Chain.d.ts +2 -2
- package/dist/tempo/session/precompile/Chain.js +8 -4
- package/dist/tempo/session/server/ChannelStore.d.ts +186 -3
- package/dist/tempo/session/server/ChannelStore.js +312 -12
- package/dist/tempo/session/server/CloseSupervisor.d.ts +98 -0
- package/dist/tempo/session/server/CloseSupervisor.js +289 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts +7 -1
- package/dist/tempo/session/server/CredentialVerification.js +220 -48
- package/dist/tempo/session/server/MeteredStream.d.ts +6 -5
- package/dist/tempo/session/server/MeteredStream.js +10 -26
- package/dist/tempo/session/server/Session.d.ts +70 -4
- package/dist/tempo/session/server/Session.js +156 -36
- package/dist/tempo/session/server/Settlement.d.ts +18 -4
- package/dist/tempo/session/server/Settlement.js +119 -32
- package/dist/tempo/session/server/SponsorshipLimit.d.ts +52 -0
- package/dist/tempo/session/server/SponsorshipLimit.js +107 -0
- package/dist/tempo/session/server/Sse.d.ts +4 -3
- package/dist/tempo/session/server/Sse.js +3 -3
- package/dist/tempo/session/server/Ws.d.ts +9 -1
- package/dist/tempo/session/server/Ws.js +1 -0
- package/dist/tempo/session/server/index.d.ts +9 -1
- package/dist/tempo/session/server/index.js +5 -1
- package/package.json +1 -1
|
@@ -22,6 +22,9 @@ export declare function markPrepaidSessionTick(receipt: SessionReceipt): Session
|
|
|
22
22
|
* - Fallback to standard HTTP receipt handling for plain Response
|
|
23
23
|
*/
|
|
24
24
|
export declare function sse(options: sse.Options & {
|
|
25
|
+
beforeCharge?: ((channelId: `0x${string}`) => Promise<void>) | undefined;
|
|
26
|
+
/** Charges the verified route's price: reusable challenges are not bound to their quoted amount. */
|
|
27
|
+
reusableChallenges?: boolean | undefined;
|
|
25
28
|
settleCharged?: SettleChargedSessionChannel | undefined;
|
|
26
29
|
store: ChannelStore.ChannelStore;
|
|
27
30
|
}): Sse;
|
|
@@ -70,7 +70,10 @@ export function sse(options) {
|
|
|
70
70
|
const verifiedRequest = envelope?.request ?? verifiedCredential.challenge.request;
|
|
71
71
|
const payload = requireSessionCredentialContext(verifiedCredential.payload, 'No SSE context available');
|
|
72
72
|
const channelId = payload.channelId;
|
|
73
|
-
|
|
73
|
+
// A reusable challenge may have quoted another request's price, so charge the verified route's
|
|
74
|
+
// canonical price. Otherwise the challenge amount is canonical and bound to the route.
|
|
75
|
+
const tickCost = BigInt((options.reusableChallenges ? verifiedRequest : verifiedCredential.challenge.request)
|
|
76
|
+
.amount);
|
|
74
77
|
const unitType = typeof verifiedRequest.unitType === 'string' ? verifiedRequest.unitType : undefined;
|
|
75
78
|
// Auto-detect upstream SSE responses and parse them into an
|
|
76
79
|
// AsyncIterable so they flow through the metered pipeline.
|
|
@@ -91,6 +94,7 @@ export function sse(options) {
|
|
|
91
94
|
tickCost,
|
|
92
95
|
pollIntervalMs: pollingInterval,
|
|
93
96
|
generate,
|
|
97
|
+
beforeCharge: () => options.beforeCharge?.(channelId),
|
|
94
98
|
onChargeCommitted: options.settleCharged,
|
|
95
99
|
prepaidUnits: hasPrepaidSessionTick(receipt) ? 1 : 0,
|
|
96
100
|
signal: input.signal,
|
|
@@ -23,6 +23,8 @@ export type ChannelEntry = {
|
|
|
23
23
|
channelId: Hex.Hex;
|
|
24
24
|
/** Highest cumulative amount this client has locally authorized. */
|
|
25
25
|
cumulativeAmount: bigint;
|
|
26
|
+
/** Blocks new deposits until an uncertain top-up has been reconciled against trusted chain state. */
|
|
27
|
+
uncertainTopUp?: boolean | undefined;
|
|
26
28
|
/** Latest channel deposit known by the client. */
|
|
27
29
|
deposit: bigint;
|
|
28
30
|
/** TIP-1034 descriptor required for vouchers, top-ups, and close credentials. */
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { Address } from 'viem';
|
|
2
2
|
import type { MaybePromise } from '../../../internal/types.js';
|
|
3
3
|
import type { ChannelEntry } from './ChannelOps.js';
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Store of reusable payer session channels keyed by payment scope.
|
|
6
|
+
* Callers sharing a backend across processes must coordinate funding and writes externally.
|
|
7
|
+
*/
|
|
5
8
|
export type ChannelStore = {
|
|
6
9
|
/** Returns the channel cached for `key`, when present. */
|
|
7
10
|
get(key: string): MaybePromise<ChannelEntry | undefined>;
|
|
@@ -48,4 +51,12 @@ export type JsonChannelKv = {
|
|
|
48
51
|
};
|
|
49
52
|
/** Wraps a string KV backend as a bigint-safe channel store. */
|
|
50
53
|
export declare function createJsonChannelStore(kv: JsonChannelKv): ChannelStore;
|
|
54
|
+
/** @internal Shares funding coordination between a manager's store view and its backing store. */
|
|
55
|
+
export declare function registerFundingStore(view: ChannelStore, backing: ChannelStore): void;
|
|
56
|
+
/** @internal Serializes funding decisions for callers sharing one backing store in this process. */
|
|
57
|
+
export declare function withFundingLock<T>(store: ChannelStore, key: string, operation: () => Promise<T>): Promise<T>;
|
|
58
|
+
/** @internal Serializes writes and preserves unresolved funding across stale channel copies. */
|
|
59
|
+
export declare function persistChannel(store: ChannelStore, entry: ChannelEntry): Promise<void>;
|
|
60
|
+
/** @internal Retains unresolved funding records through close and stale-entry cleanup. */
|
|
61
|
+
export declare function deleteChannel(store: ChannelStore, key: string): Promise<void>;
|
|
51
62
|
//# sourceMappingURL=ChannelStore.d.ts.map
|
|
@@ -60,4 +60,51 @@ export function createJsonChannelStore(kv) {
|
|
|
60
60
|
},
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
|
+
const pendingOperations = new WeakMap();
|
|
64
|
+
const fundingStores = new WeakMap();
|
|
65
|
+
/** @internal Shares funding coordination between a manager's store view and its backing store. */
|
|
66
|
+
export function registerFundingStore(view, backing) {
|
|
67
|
+
fundingStores.set(view, fundingStores.get(backing) ?? backing);
|
|
68
|
+
}
|
|
69
|
+
async function exclusive(store, key, operation) {
|
|
70
|
+
let pending = pendingOperations.get(store);
|
|
71
|
+
if (!pending) {
|
|
72
|
+
pending = new Map();
|
|
73
|
+
pendingOperations.set(store, pending);
|
|
74
|
+
}
|
|
75
|
+
const previous = pending.get(key) ?? Promise.resolve();
|
|
76
|
+
const result = previous.catch(() => { }).then(operation);
|
|
77
|
+
pending.set(key, result);
|
|
78
|
+
try {
|
|
79
|
+
return await result;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (pending.get(key) === result)
|
|
83
|
+
pending.delete(key);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** @internal Serializes funding decisions for callers sharing one backing store in this process. */
|
|
87
|
+
export function withFundingLock(store, key, operation) {
|
|
88
|
+
return exclusive(fundingStores.get(store) ?? store, `funding:${key}`, operation);
|
|
89
|
+
}
|
|
90
|
+
/** @internal Serializes writes and preserves unresolved funding across stale channel copies. */
|
|
91
|
+
export async function persistChannel(store, entry) {
|
|
92
|
+
await exclusive(store, `write:${entryKey(entry)}`, async () => {
|
|
93
|
+
const current = await store.get(entryKey(entry));
|
|
94
|
+
if (current?.uncertainTopUp) {
|
|
95
|
+
if (current.channelId !== entry.channelId)
|
|
96
|
+
throw new Error('Session funding outcome is unknown; reconcile the pending payment before replacing the channel.');
|
|
97
|
+
entry.uncertainTopUp = true;
|
|
98
|
+
}
|
|
99
|
+
await store.set(entry);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** @internal Retains unresolved funding records through close and stale-entry cleanup. */
|
|
103
|
+
export async function deleteChannel(store, key) {
|
|
104
|
+
await exclusive(store, `write:${key}`, async () => {
|
|
105
|
+
if ((await store.get(key))?.uncertainTopUp)
|
|
106
|
+
return;
|
|
107
|
+
await store.delete(key);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
63
110
|
//# sourceMappingURL=ChannelStore.js.map
|
|
@@ -7,7 +7,7 @@ import * as Chain from '../precompile/Chain.js';
|
|
|
7
7
|
import * as Channel from '../precompile/Channel.js';
|
|
8
8
|
import * as Voucher from '../precompile/Voucher.js';
|
|
9
9
|
import { createClosePayload, createOpenPayload, createTopUpPayload, createVoucherPayload, isSameAddress, resolveAuthorizedSigner, resolveEscrow, } from './ChannelOps.js';
|
|
10
|
-
import { channelKey, entryKey } from './ChannelStore.js';
|
|
10
|
+
import { channelKey, entryKey, persistChannel, deleteChannel, } from './ChannelStore.js';
|
|
11
11
|
import { assertWithinMaxDeposit, resolveOpeningDeposit } from './Runtime.js';
|
|
12
12
|
/** Returns whether a credential payload carries cumulative voucher authorization. */
|
|
13
13
|
export function hasCredentialCumulativeAmount(payload) {
|
|
@@ -26,9 +26,9 @@ export function readCredentialCumulativeAmount(payload) {
|
|
|
26
26
|
*/
|
|
27
27
|
async function storeChannelEntry(sink, entry) {
|
|
28
28
|
if (entry.opened)
|
|
29
|
-
await sink.store
|
|
29
|
+
await persistChannel(sink.store, entry);
|
|
30
30
|
else
|
|
31
|
-
await sink.store
|
|
31
|
+
await deleteChannel(sink.store, entryKey(entry));
|
|
32
32
|
sink.notifyUpdate(entry);
|
|
33
33
|
}
|
|
34
34
|
/** Applies a credential payload's cumulative amount to the stored channel at `key`. */
|
|
@@ -338,6 +338,7 @@ async function resolveAutoSwapCalls(autoSwap, parameters) {
|
|
|
338
338
|
tokenOut: parameters.tokenOut,
|
|
339
339
|
tokenIn: autoSwap.tokenIn,
|
|
340
340
|
slippage: autoSwap.slippage,
|
|
341
|
+
maxAmountIn: autoSwap.maxAmountIn,
|
|
341
342
|
}));
|
|
342
343
|
}
|
|
343
344
|
async function open(plan, sink, autoSwap) {
|
|
@@ -403,7 +404,11 @@ async function recover(plan, sink) {
|
|
|
403
404
|
throw new Error('recovered voucher amount exceeds on-chain channel deposit');
|
|
404
405
|
assertWithinMaxDeposit(cumulativeAmount, maxDeposit);
|
|
405
406
|
const payload = await createVoucherPayload(resolved.client, account, descriptor, cumulativeAmount, resolved.chainId, resolved.escrow);
|
|
407
|
+
const current = await sink.store.get(resolved.key);
|
|
406
408
|
await storeChannelEntry(sink, {
|
|
409
|
+
...(current?.channelId === reusable.channelId && current.uncertainTopUp
|
|
410
|
+
? { uncertainTopUp: true }
|
|
411
|
+
: {}),
|
|
407
412
|
channelId: reusable.channelId,
|
|
408
413
|
cumulativeAmount,
|
|
409
414
|
deposit: reusable.state.deposit,
|
|
@@ -13,7 +13,7 @@ import * as Methods from '../../Methods.js';
|
|
|
13
13
|
import * as Channel from '../precompile/Channel.js';
|
|
14
14
|
import { deserializeSessionReceipt, isEventStream, readSessionChallengeAmount, requireSessionCredentialContext, } from '../precompile/Protocol.js';
|
|
15
15
|
import { serializeCredential } from './ChannelOps.js';
|
|
16
|
-
import { createChannelStore, entryKey } from './ChannelStore.js';
|
|
16
|
+
import { createChannelStore, entryKey, persistChannel, withFundingLock, } from './ChannelStore.js';
|
|
17
17
|
import { canSignDescriptor, executeCredentialPlan, hasSessionAction, planCredential, resolveChallengeContext, resolveRecoverContext, sessionContextSchema, } from './CredentialState.js';
|
|
18
18
|
import { assertWithinMaxDeposit, resolveAutomaticTopUp } from './Runtime.js';
|
|
19
19
|
import { getSessionSnapshot, handleSseNeedVoucher, isTip1034SessionChallenge, postTopUp, wrapSseResponse, } from './Transports.js';
|
|
@@ -174,7 +174,7 @@ export function session(parameters = {}) {
|
|
|
174
174
|
const current = await store.get(resolved.key);
|
|
175
175
|
if (!current?.opened ||
|
|
176
176
|
current.channelId.toLowerCase() !== opened.channelId.toLowerCase()) {
|
|
177
|
-
await store
|
|
177
|
+
await persistChannel(store, opened);
|
|
178
178
|
sink.notifyUpdate(opened);
|
|
179
179
|
}
|
|
180
180
|
}
|
|
@@ -196,38 +196,55 @@ export function session(parameters = {}) {
|
|
|
196
196
|
},
|
|
197
197
|
});
|
|
198
198
|
const topUpChannelIfNeeded = async ({ challenge, channel, deposit, fetch, input, requiredCumulative, signal, }) => {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
createSessionCredential: (challenge, context) => method.createCredential({ challenge, context }),
|
|
216
|
-
fetch,
|
|
217
|
-
input,
|
|
218
|
-
signal,
|
|
199
|
+
return withFundingLock(store, entryKey(channel), async () => {
|
|
200
|
+
signal?.throwIfAborted();
|
|
201
|
+
const fundingChannel = await store.get(entryKey(channel));
|
|
202
|
+
if (fundingChannel?.channelId === channel.channelId)
|
|
203
|
+
channel = fundingChannel;
|
|
204
|
+
if (channel.uncertainTopUp)
|
|
205
|
+
throw new Error('Session funding outcome is unknown; reconcile the pending payment before another top-up.');
|
|
206
|
+
const knownDeposit = channel.deposit > deposit ? channel.deposit : deposit;
|
|
207
|
+
const additionalDeposit = resolveAutomaticTopUp({
|
|
208
|
+
deposit: knownDeposit,
|
|
209
|
+
maxDeposit,
|
|
210
|
+
requiredCumulative,
|
|
211
|
+
suggestedDeposit: challenge.request.suggestedDeposit === undefined
|
|
212
|
+
? undefined
|
|
213
|
+
: BigInt(challenge.request.suggestedDeposit),
|
|
214
|
+
topUpAmount,
|
|
219
215
|
});
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
216
|
+
if (additionalDeposit > 0n)
|
|
217
|
+
await postTopUp({
|
|
218
|
+
additionalDeposit,
|
|
219
|
+
challenge,
|
|
220
|
+
channel,
|
|
221
|
+
channelId: channel.channelId,
|
|
222
|
+
createSessionCredential: (challenge, context) => method.createCredential({ challenge, context }),
|
|
223
|
+
onUncertainFunding: async (entry) => {
|
|
224
|
+
const current = await store.get(entryKey(entry));
|
|
225
|
+
const unresolved = {
|
|
226
|
+
...(current?.channelId === entry.channelId ? current : entry),
|
|
227
|
+
uncertainTopUp: true,
|
|
228
|
+
};
|
|
229
|
+
await persistChannel(store, unresolved);
|
|
230
|
+
sink.notifyUpdate(unresolved);
|
|
231
|
+
},
|
|
232
|
+
fetch,
|
|
233
|
+
input,
|
|
234
|
+
signal,
|
|
235
|
+
});
|
|
236
|
+
const nextDeposit = knownDeposit + additionalDeposit;
|
|
237
|
+
if (nextDeposit === channel.deposit)
|
|
238
|
+
return;
|
|
239
|
+
const current = await store.get(entryKey(channel));
|
|
240
|
+
const latest = current?.channelId.toLowerCase() === channel.channelId.toLowerCase() ? current : channel;
|
|
241
|
+
const updated = {
|
|
242
|
+
...latest,
|
|
243
|
+
deposit: latest.deposit > nextDeposit ? latest.deposit : nextDeposit,
|
|
244
|
+
};
|
|
245
|
+
await persistChannel(store, updated);
|
|
246
|
+
sink.notifyUpdate(updated);
|
|
247
|
+
});
|
|
231
248
|
};
|
|
232
249
|
MethodChallenge.register(method, async ({ challenge, context, fetch, input, signal }) => {
|
|
233
250
|
if (!isTip1034SessionChallenge(challenge))
|
|
@@ -9,7 +9,7 @@ import * as Account from '../../../viem/Account.js';
|
|
|
9
9
|
import * as Client from '../../../viem/Client.js';
|
|
10
10
|
import { charge as chargePlugin } from '../../client/Charge.js';
|
|
11
11
|
import * as defaults from '../../internal/defaults.js';
|
|
12
|
-
import { createChannelStore, entryKey } from '../client/ChannelStore.js';
|
|
12
|
+
import { createChannelStore, deleteChannel, entryKey, persistChannel as persistStoredChannel, registerFundingStore, withFundingLock, } from '../client/ChannelStore.js';
|
|
13
13
|
import { hydrateSessionSnapshot, sessionContextSchema, } from '../client/CredentialState.js';
|
|
14
14
|
import { session as sessionPlugin } from '../client/Session.js';
|
|
15
15
|
import * as Channel from '../precompile/Channel.js';
|
|
@@ -24,7 +24,7 @@ import { closeSocketSession } from './Runtime.js';
|
|
|
24
24
|
import { closeHttpSession, getSessionSnapshot, isTempoSessionChallenge, postTopUp, retryHttpPaymentRequired, webSocketProbeUrl, } from './Transports.js';
|
|
25
25
|
import { WebSocketReadyState, } from './Transports.js';
|
|
26
26
|
import { consumeSseSessionResponse, openSseSession } from './Transports.js';
|
|
27
|
-
import { applyTopUpResult, resolveManualTopUp } from './Transports.js';
|
|
27
|
+
import { UncertainFundingError, applyTopUpResult, resolveManualTopUp, } from './Transports.js';
|
|
28
28
|
import { openWebSocketSession, prepareWebSocketSession, probeWebSocketSession, } from './Transports.js';
|
|
29
29
|
export { computeFallbackCloseAmount } from './Runtime.js';
|
|
30
30
|
function isTempoChargeChallenge(challenge) {
|
|
@@ -117,6 +117,7 @@ export function sessionManager(parameters) {
|
|
|
117
117
|
return entry;
|
|
118
118
|
return undefined;
|
|
119
119
|
}
|
|
120
|
+
const persistChannel = (entry) => persistStoredChannel(backing, entry);
|
|
120
121
|
const store = {
|
|
121
122
|
async get(key) {
|
|
122
123
|
const entry = await getReusable(key);
|
|
@@ -133,26 +134,34 @@ export function sessionManager(parameters) {
|
|
|
133
134
|
ignoredChannelIds.delete(entry.channelId);
|
|
134
135
|
if (channelUse?.trackCreates && !channelUse.seenExisting.has(key))
|
|
135
136
|
channelUse.created.set(key, entry);
|
|
136
|
-
await
|
|
137
|
+
await persistChannel(entry);
|
|
137
138
|
},
|
|
138
|
-
delete: (key) => backing
|
|
139
|
+
delete: (key) => deleteChannel(backing, key),
|
|
139
140
|
};
|
|
141
|
+
registerFundingStore(store, backing);
|
|
140
142
|
/** Removes a failed channel from candidacy for the rest of this manager's life. */
|
|
141
143
|
async function ignoreChannel(entry) {
|
|
142
144
|
const key = entryKey(entry);
|
|
145
|
+
const current = await backing.get(key);
|
|
146
|
+
if (entry.uncertainTopUp || current?.uncertainTopUp)
|
|
147
|
+
throw new Error('Session funding outcome is unknown; reconcile the pending payment before replacing the channel.');
|
|
143
148
|
ignoredChannelIds.add(entry.channelId);
|
|
144
149
|
channelUse?.seenExisting.delete(key);
|
|
145
150
|
if (channelUse?.resumed === entry)
|
|
146
151
|
channelUse.resumed = undefined;
|
|
147
|
-
await Promise.resolve(backing
|
|
152
|
+
await Promise.resolve(deleteChannel(backing, key)).catch(() => undefined);
|
|
148
153
|
}
|
|
149
154
|
async function rollbackCreatedChannels() {
|
|
150
155
|
const use = channelUse;
|
|
151
156
|
if (!use?.created.size)
|
|
152
157
|
return;
|
|
153
158
|
for (const [key, entry] of use.created) {
|
|
159
|
+
const current = await backing.get(key);
|
|
160
|
+
if (entry.uncertainTopUp ||
|
|
161
|
+
(current?.channelId === entry.channelId && current.uncertainTopUp))
|
|
162
|
+
throw new Error('Session funding outcome is unknown; reconcile the pending payment before discarding the channel.');
|
|
154
163
|
ignoredChannelIds.add(entry.channelId);
|
|
155
|
-
await Promise.resolve(backing
|
|
164
|
+
await Promise.resolve(deleteChannel(backing, key)).catch(() => undefined);
|
|
156
165
|
}
|
|
157
166
|
use.created.clear();
|
|
158
167
|
await restoreRuntime(use.committed ?? use.previous);
|
|
@@ -193,7 +202,7 @@ export function sessionManager(parameters) {
|
|
|
193
202
|
const baseline = use?.committed?.channel?.entry ?? use?.resumed;
|
|
194
203
|
if (!use ||
|
|
195
204
|
baseline?.channelId.toLowerCase() !== entry.channelId.toLowerCase() ||
|
|
196
|
-
entry.deposit <= baseline.deposit)
|
|
205
|
+
(entry.deposit <= baseline.deposit && !entry.uncertainTopUp))
|
|
197
206
|
return;
|
|
198
207
|
use.resumed = undefined;
|
|
199
208
|
use.committed = captureRuntimeStateSnapshot({
|
|
@@ -230,6 +239,9 @@ export function sessionManager(parameters) {
|
|
|
230
239
|
commitDurableTopUp(entry);
|
|
231
240
|
},
|
|
232
241
|
});
|
|
242
|
+
const handleMethodChallenge = MethodChallenge.get(method);
|
|
243
|
+
if (handleMethodChallenge)
|
|
244
|
+
MethodChallenge.register(method, (parameters) => withTopUpLock(() => Promise.resolve(handleMethodChallenge(parameters)), parameters.signal));
|
|
233
245
|
MethodResponse.unregister(method);
|
|
234
246
|
const chargeMethod = chargePlugin({
|
|
235
247
|
allowedChainIds: parameters.allowedChainIds,
|
|
@@ -435,60 +447,155 @@ export function sessionManager(parameters) {
|
|
|
435
447
|
maxVoucherCumulative: config.maxVoucherCumulative,
|
|
436
448
|
});
|
|
437
449
|
}
|
|
450
|
+
let pendingTopUp;
|
|
451
|
+
let uncertainFunding;
|
|
452
|
+
// Keep funding decisions and their deposit updates atomic within this manager.
|
|
453
|
+
// Waiters share failures instead of immediately retrying an uncertain payment.
|
|
454
|
+
async function withTopUpLock(operation, signal) {
|
|
455
|
+
if (uncertainFunding)
|
|
456
|
+
throw uncertainFunding;
|
|
457
|
+
for (;;) {
|
|
458
|
+
const pending = pendingTopUp;
|
|
459
|
+
if (!pending)
|
|
460
|
+
break;
|
|
461
|
+
if (!signal)
|
|
462
|
+
await pending;
|
|
463
|
+
else {
|
|
464
|
+
signal.throwIfAborted();
|
|
465
|
+
let abort;
|
|
466
|
+
try {
|
|
467
|
+
await Promise.race([
|
|
468
|
+
pending,
|
|
469
|
+
new Promise((_, reject) => {
|
|
470
|
+
abort = () => reject(signal.reason);
|
|
471
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
472
|
+
}),
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
signal.removeEventListener('abort', abort);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
signal?.throwIfAborted();
|
|
481
|
+
if (uncertainFunding)
|
|
482
|
+
throw uncertainFunding;
|
|
483
|
+
const pending = operation();
|
|
484
|
+
pendingTopUp = pending;
|
|
485
|
+
try {
|
|
486
|
+
return await pending;
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
if (error instanceof UncertainFundingError)
|
|
490
|
+
uncertainFunding = new Error('Session funding outcome is unknown; reconcile the pending payment before creating a new session manager.');
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
if (pendingTopUp === pending)
|
|
495
|
+
pendingTopUp = undefined;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
438
498
|
async function postTopUpAndApply(parameters) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
entry: applied.channel,
|
|
463
|
-
spent: runtime.spent.toString(),
|
|
464
|
-
units: activeUnits(),
|
|
499
|
+
if (!runtime.channel)
|
|
500
|
+
throw new Error('Cannot top up session: no active channel');
|
|
501
|
+
return withFundingLock(backing, entryKey(runtime.channel), async () => {
|
|
502
|
+
const { knownDeposit, ...topUp } = parameters;
|
|
503
|
+
const stored = runtime.channel ? await store.get(entryKey(runtime.channel)) : undefined;
|
|
504
|
+
if (stored?.channelId === parameters.channelId && stored.uncertainTopUp)
|
|
505
|
+
throw new Error('Session funding outcome is unknown; reconcile the pending payment before another top-up.');
|
|
506
|
+
const receipt = await postTopUp({
|
|
507
|
+
...topUp,
|
|
508
|
+
channel: runtime.channel,
|
|
509
|
+
createSessionCredential,
|
|
510
|
+
fetch: config.fetch,
|
|
511
|
+
onUncertainFunding: async (channel) => {
|
|
512
|
+
const current = await store.get(entryKey(channel));
|
|
513
|
+
const unresolved = {
|
|
514
|
+
...(current?.channelId === channel.channelId ? current : channel),
|
|
515
|
+
uncertainTopUp: true,
|
|
516
|
+
};
|
|
517
|
+
if (runtime.channel?.channelId === channel.channelId)
|
|
518
|
+
runtime.channel = unresolved;
|
|
519
|
+
await store.set(unresolved);
|
|
520
|
+
commitDurableTopUp(unresolved);
|
|
521
|
+
},
|
|
465
522
|
});
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
523
|
+
updateSpentFromReceipt(receipt);
|
|
524
|
+
let baseline = runtime.channel;
|
|
525
|
+
const applied = applyTopUpResult({
|
|
526
|
+
additionalDeposit: parameters.additionalDeposit,
|
|
527
|
+
channel: runtime.channel,
|
|
528
|
+
channelId: parameters.channelId,
|
|
529
|
+
challengeId: runtime.lastChallenge?.id,
|
|
530
|
+
currentState: runtime.state,
|
|
531
|
+
knownDeposit,
|
|
532
|
+
receipt,
|
|
533
|
+
spent: runtime.spent,
|
|
534
|
+
});
|
|
535
|
+
if (applied?.channel && runtime.lastChallenge) {
|
|
536
|
+
let entry = applied.channel;
|
|
537
|
+
for (;;) {
|
|
538
|
+
await store.set(entry);
|
|
539
|
+
const current = runtime.channel;
|
|
540
|
+
if (current === baseline)
|
|
541
|
+
break;
|
|
542
|
+
// A completed deposit must not restore a replaced session. For the same
|
|
543
|
+
// channel retain newer vouchers while recording the confirmed funding.
|
|
544
|
+
if (!current)
|
|
545
|
+
return receipt;
|
|
546
|
+
const funded = applied.channel;
|
|
547
|
+
entry =
|
|
548
|
+
current.channelId !== funded.channelId
|
|
549
|
+
? current
|
|
550
|
+
: {
|
|
551
|
+
...current,
|
|
552
|
+
deposit: current.deposit > funded.deposit ? current.deposit : funded.deposit,
|
|
553
|
+
cumulativeAmount: current.cumulativeAmount > funded.cumulativeAmount
|
|
554
|
+
? current.cumulativeAmount
|
|
555
|
+
: funded.cumulativeAmount,
|
|
556
|
+
};
|
|
557
|
+
baseline = current;
|
|
558
|
+
}
|
|
559
|
+
if (entry.channelId !== applied.channel.channelId)
|
|
560
|
+
return receipt;
|
|
561
|
+
runtime.channel = entry;
|
|
562
|
+
dispatch({
|
|
563
|
+
type: 'activated',
|
|
564
|
+
challengeId: runtime.lastChallenge.id,
|
|
565
|
+
entry,
|
|
566
|
+
spent: runtime.spent.toString(),
|
|
567
|
+
units: activeUnits(),
|
|
568
|
+
});
|
|
569
|
+
commitDurableTopUp(entry);
|
|
570
|
+
}
|
|
571
|
+
return receipt;
|
|
572
|
+
});
|
|
469
573
|
}
|
|
470
574
|
async function topUpIfNeeded(parameters) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
deposit
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
:
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
575
|
+
return withTopUpLock(async () => {
|
|
576
|
+
parameters.signal?.throwIfAborted();
|
|
577
|
+
const channelDeposit = runtime.channel?.channelId === parameters.channelId ? runtime.channel.deposit : 0n;
|
|
578
|
+
const deposit = channelDeposit > parameters.deposit ? channelDeposit : parameters.deposit;
|
|
579
|
+
const additionalDeposit = resolveAutomaticTopUp({
|
|
580
|
+
deposit,
|
|
581
|
+
maxDeposit: config.maxVoucherCumulative,
|
|
582
|
+
requiredCumulative: parameters.requiredCumulative,
|
|
583
|
+
suggestedDeposit: parameters.challenge.request.suggestedDeposit === undefined
|
|
584
|
+
? undefined
|
|
585
|
+
: BigInt(parameters.challenge.request.suggestedDeposit),
|
|
586
|
+
topUpAmount: config.topUpAmount,
|
|
587
|
+
});
|
|
588
|
+
if (additionalDeposit === 0n)
|
|
589
|
+
return;
|
|
590
|
+
await postTopUpAndApply({
|
|
591
|
+
challenge: parameters.challenge,
|
|
592
|
+
input: parameters.input,
|
|
593
|
+
channelId: parameters.channelId,
|
|
594
|
+
additionalDeposit,
|
|
595
|
+
knownDeposit: deposit,
|
|
596
|
+
signal: parameters.signal,
|
|
597
|
+
});
|
|
598
|
+
}, parameters.signal);
|
|
492
599
|
}
|
|
493
600
|
async function restoreCumulative(channelId, cumulativeAmount) {
|
|
494
601
|
const restored = restoreCumulativeAuthorization({
|
|
@@ -507,7 +614,7 @@ export function sessionManager(parameters) {
|
|
|
507
614
|
spent: restored.spent,
|
|
508
615
|
units: restored.units,
|
|
509
616
|
});
|
|
510
|
-
await
|
|
617
|
+
await persistChannel(runtime.channel);
|
|
511
618
|
}
|
|
512
619
|
}
|
|
513
620
|
async function restoreRuntime(snapshot) {
|
|
@@ -516,7 +623,7 @@ export function sessionManager(parameters) {
|
|
|
516
623
|
runtime.spent = restored.spent;
|
|
517
624
|
runtime.state = restored.state;
|
|
518
625
|
if (runtime.channel?.opened)
|
|
519
|
-
await
|
|
626
|
+
await persistChannel(runtime.channel);
|
|
520
627
|
}
|
|
521
628
|
function toPaymentResponse(response) {
|
|
522
629
|
const receiptHeader = response.headers.get(Constants.Headers.paymentReceipt);
|
|
@@ -667,7 +774,7 @@ export function sessionManager(parameters) {
|
|
|
667
774
|
},
|
|
668
775
|
fetch: doFetch,
|
|
669
776
|
async topUp(amount) {
|
|
670
|
-
const
|
|
777
|
+
const intended = resolveManualTopUp({
|
|
671
778
|
amount,
|
|
672
779
|
assertVoucherWithinLocalLimit,
|
|
673
780
|
channel: runtime.channel,
|
|
@@ -675,11 +782,25 @@ export function sessionManager(parameters) {
|
|
|
675
782
|
lastChallenge: runtime.lastChallenge,
|
|
676
783
|
lastUrl: runtime.lastUrl,
|
|
677
784
|
});
|
|
678
|
-
return
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
785
|
+
return withTopUpLock(async () => {
|
|
786
|
+
if (runtime.channel?.channelId !== intended.channelId ||
|
|
787
|
+
runtime.lastChallenge !== intended.challenge ||
|
|
788
|
+
runtime.lastUrl !== intended.input)
|
|
789
|
+
throw new Error('Session changed while waiting to top up');
|
|
790
|
+
const target = resolveManualTopUp({
|
|
791
|
+
amount,
|
|
792
|
+
assertVoucherWithinLocalLimit,
|
|
793
|
+
channel: runtime.channel,
|
|
794
|
+
decimals: config.decimals,
|
|
795
|
+
lastChallenge: intended.challenge,
|
|
796
|
+
lastUrl: intended.input,
|
|
797
|
+
});
|
|
798
|
+
return postTopUpAndApply({
|
|
799
|
+
additionalDeposit: target.additionalDeposit,
|
|
800
|
+
challenge: target.challenge,
|
|
801
|
+
channelId: target.channelId,
|
|
802
|
+
input: target.input,
|
|
803
|
+
});
|
|
683
804
|
});
|
|
684
805
|
},
|
|
685
806
|
async sse(input, init) {
|
|
@@ -213,6 +213,8 @@ export type PostTopUpParameters = {
|
|
|
213
213
|
input: RequestInfo | URL;
|
|
214
214
|
/** Cancels the top-up management request. */
|
|
215
215
|
signal?: AbortSignal | undefined;
|
|
216
|
+
/** Persists an unresolved funding outcome before another caller can reuse the channel. */
|
|
217
|
+
onUncertainFunding?: ((channel: ChannelEntry) => Promise<void>) | undefined;
|
|
216
218
|
};
|
|
217
219
|
/** Inputs for retrying an HTTP 402 with a top-up/voucher management round trip. */
|
|
218
220
|
export type RetryHttpPaymentRequiredParameters = {
|
|
@@ -277,6 +279,10 @@ export declare function managementInput(input: RequestInfo | URL): RequestInfo |
|
|
|
277
279
|
export declare function webSocketProbeUrl(input: string | URL): URL;
|
|
278
280
|
/** Reads an HTTP problem detail body, falling back to raw text for non-problem responses. */
|
|
279
281
|
export declare function readProblemDetail(response: Response): Promise<string>;
|
|
282
|
+
/** A dispatched deposit may have executed despite a failed or cancelled response. */
|
|
283
|
+
export declare class UncertainFundingError extends Error {
|
|
284
|
+
constructor(reason: unknown);
|
|
285
|
+
}
|
|
280
286
|
/** Posts a top-up management credential and returns its receipt, when present. */
|
|
281
287
|
export declare function postTopUp(parameters: PostTopUpParameters): Promise<SessionReceipt | undefined>;
|
|
282
288
|
/** Retries an HTTP 402 when the server snapshot asks for more voucher headroom. */
|