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
|
@@ -1,38 +1,24 @@
|
|
|
1
|
-
import { commitReservedCharges, releaseReservedCharges,
|
|
1
|
+
import { commitReservedCharges, releaseReservedCharges, reserveChargeOrWait } from './Transports.js';
|
|
2
2
|
/** Applies voucher reservation and spend commits to an async session stream. */
|
|
3
3
|
export async function* meterIterable(options) {
|
|
4
|
+
const manual = typeof options.generate === 'function';
|
|
4
5
|
let prepaidUnits = options.prepaidUnits ?? 0;
|
|
5
|
-
const
|
|
6
|
-
const signal = controller.signal;
|
|
6
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
7
7
|
const reservation = {
|
|
8
8
|
store: options.store,
|
|
9
9
|
channelId: options.channelId,
|
|
10
10
|
reservationId: globalThis.crypto.randomUUID(),
|
|
11
11
|
};
|
|
12
12
|
let pending = false;
|
|
13
|
-
let cancelled = false;
|
|
14
|
-
const onAbort = () => {
|
|
15
|
-
if (signal.aborted)
|
|
16
|
-
return;
|
|
17
|
-
cancelled = true;
|
|
18
|
-
controller.abort(options.signal?.reason);
|
|
19
|
-
};
|
|
20
|
-
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
21
|
-
if (options.signal?.aborted)
|
|
22
|
-
onAbort();
|
|
23
|
-
const renewal = setInterval(() => {
|
|
24
|
-
if (pending)
|
|
25
|
-
void renewReservedCharges(reservation).catch((error) => controller.abort(error));
|
|
26
|
-
}, streamReservationLeaseMs / 3);
|
|
27
|
-
renewal.unref?.();
|
|
28
13
|
const releaseOnAbort = () => {
|
|
29
|
-
clearInterval(renewal);
|
|
30
14
|
void releaseReservedCharges(reservation).catch(() => { });
|
|
31
15
|
};
|
|
32
16
|
signal.addEventListener('abort', releaseOnAbort, { once: true });
|
|
33
17
|
if (signal.aborted)
|
|
34
18
|
releaseOnAbort();
|
|
35
19
|
const charge = async (amount) => {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
await options.beforeCharge?.();
|
|
36
22
|
signal.throwIfAborted();
|
|
37
23
|
if (amount === undefined && prepaidUnits > 0) {
|
|
38
24
|
prepaidUnits -= 1;
|
|
@@ -50,9 +36,13 @@ export async function* meterIterable(options) {
|
|
|
50
36
|
signal,
|
|
51
37
|
});
|
|
52
38
|
pending = true;
|
|
39
|
+
if (manual)
|
|
40
|
+
await commit();
|
|
53
41
|
signal.throwIfAborted();
|
|
54
42
|
};
|
|
55
43
|
const commit = async () => {
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
await options.beforeCharge?.();
|
|
56
46
|
signal.throwIfAborted();
|
|
57
47
|
if (!pending)
|
|
58
48
|
return;
|
|
@@ -63,7 +53,6 @@ export async function* meterIterable(options) {
|
|
|
63
53
|
await options.onChargeCommitted?.(channel);
|
|
64
54
|
};
|
|
65
55
|
try {
|
|
66
|
-
const manual = typeof options.generate === 'function';
|
|
67
56
|
const iterable = typeof options.generate === 'function'
|
|
68
57
|
? options.generate({ charge, signal })
|
|
69
58
|
: options.generate;
|
|
@@ -74,7 +63,7 @@ export async function* meterIterable(options) {
|
|
|
74
63
|
if (signal.aborted)
|
|
75
64
|
break;
|
|
76
65
|
if (done) {
|
|
77
|
-
if (manual)
|
|
66
|
+
if (manual && pending)
|
|
78
67
|
await commit();
|
|
79
68
|
break;
|
|
80
69
|
}
|
|
@@ -83,18 +72,13 @@ export async function* meterIterable(options) {
|
|
|
83
72
|
await commit();
|
|
84
73
|
yield value;
|
|
85
74
|
}
|
|
86
|
-
if (!cancelled)
|
|
87
|
-
signal.throwIfAborted();
|
|
88
75
|
}
|
|
89
76
|
finally {
|
|
90
|
-
clearInterval(renewal);
|
|
91
77
|
await releaseReservedCharges(reservation);
|
|
92
78
|
await iterator.return?.();
|
|
93
79
|
}
|
|
94
80
|
}
|
|
95
81
|
finally {
|
|
96
|
-
clearInterval(renewal);
|
|
97
|
-
options.signal?.removeEventListener('abort', onAbort);
|
|
98
82
|
signal.removeEventListener('abort', releaseOnAbort);
|
|
99
83
|
await releaseReservedCharges(reservation);
|
|
100
84
|
}
|
|
@@ -18,9 +18,12 @@ import * as Transport from '../../server/internal/transport.js';
|
|
|
18
18
|
import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot } from '../Snapshot.js';
|
|
19
19
|
import * as ChannelStore from './ChannelStore.js';
|
|
20
20
|
import { type ResolveSessionChannelId } from './RequestState.js';
|
|
21
|
-
import { type SettleChargedSessionChannel } from './Settlement.js';
|
|
21
|
+
import { type SessionStoreInput, type SettleChargedSessionChannel } from './Settlement.js';
|
|
22
22
|
import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
|
|
23
|
+
import * as SponsorshipLimit from './SponsorshipLimit.js';
|
|
23
24
|
import * as Ws from './Ws.js';
|
|
25
|
+
/** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
|
|
26
|
+
export declare function verifyStream(method: object, channelId: Hex): Promise<void>;
|
|
24
27
|
/** Server-side automatic settlement schedule. */
|
|
25
28
|
export type { SettlementSchedule } from './Settlement.js';
|
|
26
29
|
/** Server-side settlement event hook types. */
|
|
@@ -131,15 +134,27 @@ export declare namespace session {
|
|
|
131
134
|
serveWebSocket: (options: ServeWebSocketOptions) => Promise<void>;
|
|
132
135
|
};
|
|
133
136
|
/** WebSocket server options supplied after the session binds shared dependencies. */
|
|
134
|
-
type ServeWebSocketOptions = Omit<Ws.serve.Options, 'onChargeCommitted' | 'settleScheduled' | 'store'>;
|
|
137
|
+
type ServeWebSocketOptions = Omit<Ws.serve.Options, 'beforeCharge' | 'onChargeCommitted' | 'settleScheduled' | 'store'>;
|
|
135
138
|
/** Request defaults inferred from the Tempo session method schema. */
|
|
136
139
|
type Defaults = LooseOmit<Method.RequestDefaults<typeof Methods.session>, 'escrowContract' | 'feePayer' | 'recipient'>;
|
|
137
140
|
/** Partial fee-sponsor policy used for server-submitted session transactions. */
|
|
138
141
|
type FeePayerPolicy = Partial<FeePayer.Policy>;
|
|
139
142
|
/** Parameters accepted by the TIP-1034 server session payment method. */
|
|
140
143
|
type Parameters = {
|
|
141
|
-
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation
|
|
144
|
+
/** TTL in milliseconds for cached on-chain channel state during non-mutating validation, and before a voucher accepted under `voucherStateMaxAge` starts a fresh read. @default 5_000 */
|
|
142
145
|
channelStateTtl?: number | undefined;
|
|
146
|
+
/**
|
|
147
|
+
* Maximum age in milliseconds of verified on-chain channel state that a voucher may be validated
|
|
148
|
+
* and accepted against without waiting for a fresh read. The fresh read then finishes in the background and
|
|
149
|
+
* records a pending close, so later vouchers are rejected. `0` reads on-chain state before every
|
|
150
|
+
* acceptance.
|
|
151
|
+
*
|
|
152
|
+
* A payer can withdraw only after the escrow's `CLOSE_GRACE_PERIOD` (900 s on Tempo) following a
|
|
153
|
+
* close request, so vouchers accepted this way stay settleable as long as the payee settles them
|
|
154
|
+
* within the grace period minus this age. Keep it well below that bound.
|
|
155
|
+
* @default 0
|
|
156
|
+
*/
|
|
157
|
+
voucherStateMaxAge?: number | undefined;
|
|
143
158
|
/** Override the fee-sponsor policy used for sponsored open/topUp transactions and server-driven close transactions. */
|
|
144
159
|
feePayerPolicy?: FeePayerPolicy | undefined;
|
|
145
160
|
/** Minimum voucher delta to accept (numeric string, default: "0"). */
|
|
@@ -159,6 +174,19 @@ export declare namespace session {
|
|
|
159
174
|
* containing the signed highest voucher for client rehydration.
|
|
160
175
|
*/
|
|
161
176
|
bootstrap?: boolean | undefined;
|
|
177
|
+
/**
|
|
178
|
+
* Lets a session challenge pay for later requests to the same route until it expires. The
|
|
179
|
+
* challenge is bound to its route, currency, recipient and channel terms but not to the amount
|
|
180
|
+
* it quoted, and every request is charged its own price from the channel's accepted balance.
|
|
181
|
+
* A client can then send a voucher with a request instead of waiting for a `402`.
|
|
182
|
+
*/
|
|
183
|
+
reusableChallenges?: boolean | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* Charges request-priced content (`unitType: 'request'`) in the same store update that accepts
|
|
186
|
+
* its voucher, before the response, instead of in a second update when the response is
|
|
187
|
+
* delivered. The server must then refund requests it cannot serve.
|
|
188
|
+
*/
|
|
189
|
+
chargeOnAcceptance?: boolean | undefined;
|
|
162
190
|
/**
|
|
163
191
|
* Atomic store backend for channel state.
|
|
164
192
|
*
|
|
@@ -181,6 +209,14 @@ export declare namespace session {
|
|
|
181
209
|
settlementSchedule?: SettlementSchedule | undefined;
|
|
182
210
|
/** Optional fee token for management and server-driven settle/close transactions. */
|
|
183
211
|
feeToken?: Address | undefined;
|
|
212
|
+
/**
|
|
213
|
+
* Limits fee-payer-sponsored channel opens and top-ups to `perDay` of each per payer per UTC
|
|
214
|
+
* day (default 5), counted in `store`. The payer is the verified signer of the transaction, never
|
|
215
|
+
* the credential's client-supplied `source`. A transaction over the limit, or one whose
|
|
216
|
+
* signatures prove no payer, is broadcast unsponsored, so it lands only if the payer pays its
|
|
217
|
+
* own fees.
|
|
218
|
+
*/
|
|
219
|
+
sponsorshipLimit?: SponsorshipLimit.Parameters | undefined;
|
|
184
220
|
} & Account.resolve.Parameters & Client.getResolver.Parameters & Defaults & Method.ComposableHooks<typeof Methods.session>;
|
|
185
221
|
/** Defaults derived from `session()` parameters for handler type inference. */
|
|
186
222
|
type DeriveDefaults<parameters extends Parameters> = types.DeriveDefaults<parameters, Defaults, {
|
|
@@ -196,6 +232,36 @@ export declare namespace session {
|
|
|
196
232
|
*
|
|
197
233
|
* Delegates to the shared `deductFromChannel` atomic helper and translates
|
|
198
234
|
* failure modes into typed errors (`InsufficientBalanceError`, `ChannelClosedError`).
|
|
235
|
+
*
|
|
236
|
+
* With `options.idempotencyKey` the channel is charged at most once per key: a retry with the same
|
|
237
|
+
* key and amount returns the channel without charging again, and the same key with a different
|
|
238
|
+
* amount throws `IdempotencyKeyConflictError`.
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```ts
|
|
242
|
+
* // One durable job per voucher, retried under its operation ID.
|
|
243
|
+
* const channel = await Session.Server.charge(store, channelId, jobCost, {
|
|
244
|
+
* idempotencyKey: operationId,
|
|
245
|
+
* })
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
export declare function charge(store: SessionStoreInput, channelId: Hex, amount: bigint, options?: ChannelStore.ChargeOptions): Promise<ChannelStore.State>;
|
|
249
|
+
/**
|
|
250
|
+
* Returns an unused amount to a channel's `spent` at most once per idempotency key.
|
|
251
|
+
*
|
|
252
|
+
* The first outcome for a key is final. A credit is `declined` when the channel is closing or
|
|
253
|
+
* finalized, or when it would take `spent` below zero or below what settled, or may still settle,
|
|
254
|
+
* on-chain, because a close captures `max(spent, settled)`; the caller then refunds another way.
|
|
255
|
+
* `unknown` reports a store write that failed and may have committed; retrying with the same key
|
|
256
|
+
* returns the outcome that was recorded.
|
|
257
|
+
*
|
|
258
|
+
* @example
|
|
259
|
+
* ```ts
|
|
260
|
+
* const result = await Session.Server.credit(store, channelId, unused, {
|
|
261
|
+
* idempotencyKey: `refund:${flowId}`,
|
|
262
|
+
* })
|
|
263
|
+
* if (result.status === 'declined') await refundAnotherWay()
|
|
264
|
+
* ```
|
|
199
265
|
*/
|
|
200
|
-
export declare function
|
|
266
|
+
export declare function credit(store: SessionStoreInput, channelId: Hex, amount: bigint, options: ChannelStore.CreditOptions): Promise<ChannelStore.CreditResult>;
|
|
201
267
|
//# sourceMappingURL=Session.d.ts.map
|
|
@@ -20,17 +20,27 @@ import * as Methods from '../../Methods.js';
|
|
|
20
20
|
import * as ChargeServer from '../../server/Charge.js';
|
|
21
21
|
import { fallbackStore } from '../../server/internal/fallback-store.js';
|
|
22
22
|
import * as Transport from '../../server/internal/transport.js';
|
|
23
|
+
import * as Chain from '../precompile/Chain.js';
|
|
23
24
|
import { tip20ChannelEscrow } from '../precompile/Protocol.js';
|
|
24
25
|
import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as serializeSessionSnapshot, } from '../Snapshot.js';
|
|
25
26
|
import * as ChannelStore from './ChannelStore.js';
|
|
26
27
|
import { broadcastCredentialPayload, validateCredentialPayload } from './CredentialVerification.js';
|
|
27
|
-
import { requireSessionCredentialPayload } from './CredentialVerification.js';
|
|
28
|
+
import { requireSessionCredentialPayload, validateChannelState } from './CredentialVerification.js';
|
|
28
29
|
import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
|
|
29
30
|
import { respondToSessionCredential } from './RequestState.js';
|
|
30
|
-
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
|
|
31
|
+
import { applyVerifiedHttpAccounting, chargeSessionChannel, resolveChannelStore, shouldApplyVerifiedHttpAccounting, shouldChargeVoucherOnAcceptance, } from './Settlement.js';
|
|
31
32
|
import { ignoreRetryableSettlementFailure, isSettlementDue, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
32
33
|
import { resolveSettlementSchedule, } from './Settlement.js';
|
|
34
|
+
import * as SponsorshipLimit from './SponsorshipLimit.js';
|
|
33
35
|
import * as Ws from './Ws.js';
|
|
36
|
+
const streamVerifiers = new WeakMap();
|
|
37
|
+
/** @internal Reuses a method's escrow verifier when composing shared currency helpers. */
|
|
38
|
+
export function verifyStream(method, channelId) {
|
|
39
|
+
const verify = streamVerifiers.get(method);
|
|
40
|
+
if (!verify)
|
|
41
|
+
throw new Error('Session stream verifier is not configured');
|
|
42
|
+
return verify(channelId);
|
|
43
|
+
}
|
|
34
44
|
export { settle, settleBatch } from './Settlement.js';
|
|
35
45
|
function deriveServerDefaults(values) {
|
|
36
46
|
// `Method.toServer()` models defaults from request input fields. Tempo session
|
|
@@ -191,11 +201,25 @@ async function handleBootstrapPreflight(parameters) {
|
|
|
191
201
|
}
|
|
192
202
|
return new Response(null, { status: 204, headers });
|
|
193
203
|
}
|
|
204
|
+
/** Binds a reusable session challenge to its route and channel terms but not to its quoted amount. */
|
|
205
|
+
const reusableSessionBinding = (request) => {
|
|
206
|
+
const { methodDetails } = request;
|
|
207
|
+
return {
|
|
208
|
+
chainId: methodDetails.chainId,
|
|
209
|
+
currency: request.currency,
|
|
210
|
+
escrowContract: methodDetails.escrowContract,
|
|
211
|
+
operator: methodDetails.operator,
|
|
212
|
+
recipient: request.recipient,
|
|
213
|
+
sessionProtocol: methodDetails[Constants.MethodDetailKeys.sessionProtocol],
|
|
214
|
+
unitType: request.unitType,
|
|
215
|
+
};
|
|
216
|
+
};
|
|
194
217
|
/** Creates a server-side TIP20EscrowChannel precompile session payment method. */
|
|
195
218
|
export function session(p) {
|
|
196
219
|
const parameters = p;
|
|
197
220
|
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore = fallbackStore('tempo.session'), suggestedDeposit, unitType, } = parameters;
|
|
198
221
|
const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals);
|
|
222
|
+
SponsorshipLimit.assertParameters(parameters.sponsorshipLimit);
|
|
199
223
|
const onSessionSettlement = parameters.onSessionSettlement;
|
|
200
224
|
const onSessionSettlementFailure = parameters.onSessionSettlementFailure;
|
|
201
225
|
const store = ChannelStore.fromStore(rawStore);
|
|
@@ -236,8 +260,33 @@ export function session(p) {
|
|
|
236
260
|
// A failed scheduled settlement is reported and retried by the next one; the charged request
|
|
237
261
|
// stays served because the payer's voucher already covers the charge.
|
|
238
262
|
const settleCharged = (channel) => settleScheduled(channel).catch(ignoreRetryableSettlementFailure);
|
|
263
|
+
// Stream checks have a bounded TTL even when callers disable validation caching.
|
|
264
|
+
// Cache only successful reads; RPC failures must stop delivery.
|
|
265
|
+
const beforeStreamCharge = async (channelId) => {
|
|
266
|
+
channelId = ChannelStore.normalizeChannelId(channelId);
|
|
267
|
+
const channel = await store.getChannel(channelId);
|
|
268
|
+
if (!channel || channel.finalized || channel.closeRequestedAt !== 0n)
|
|
269
|
+
throw new Errors.ChannelClosedError({ reason: 'stream channel is closed' });
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const ttl = Math.min(Math.max(channelStateTtl, 0), 5_000);
|
|
272
|
+
const lastVerified = lastOnChainVerified.get(channelId);
|
|
273
|
+
if (lastVerified !== undefined && now >= lastVerified && now - lastVerified < ttl)
|
|
274
|
+
return;
|
|
275
|
+
let state;
|
|
276
|
+
try {
|
|
277
|
+
const client = await getClient({ chainId: channel.chainId });
|
|
278
|
+
state = await Chain.getChannelState(client, channelId, channel.escrowContract);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
// Transport errors can include private RPC URLs or authentication details.
|
|
282
|
+
throw new Error('Unable to verify stream escrow');
|
|
283
|
+
}
|
|
284
|
+
validateChannelState(state);
|
|
285
|
+
lastOnChainVerified.set(channelId, now);
|
|
286
|
+
};
|
|
239
287
|
const serveWebSocket = (options) => Ws.serve({
|
|
240
288
|
...options,
|
|
289
|
+
beforeCharge: beforeStreamCharge,
|
|
241
290
|
onChargeCommitted: settleCharged,
|
|
242
291
|
store,
|
|
243
292
|
});
|
|
@@ -253,9 +302,11 @@ export function session(p) {
|
|
|
253
302
|
});
|
|
254
303
|
const transport = parameters.sse
|
|
255
304
|
? Transport.sse({
|
|
305
|
+
...(typeof parameters.sse === 'object' ? parameters.sse : {}),
|
|
306
|
+
beforeCharge: beforeStreamCharge,
|
|
307
|
+
reusableChallenges: parameters.reusableChallenges,
|
|
256
308
|
settleCharged,
|
|
257
309
|
store,
|
|
258
|
-
...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
|
|
259
310
|
})
|
|
260
311
|
: undefined;
|
|
261
312
|
const validateCredential = async ({ credential, operation, request, }) => {
|
|
@@ -267,24 +318,31 @@ export function session(p) {
|
|
|
267
318
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
268
319
|
request,
|
|
269
320
|
});
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
321
|
+
// A broadcast voucher is checked against the stored channel by the update that accepts it.
|
|
322
|
+
const details = operation === 'broadcast' && payload.action === 'voucher'
|
|
323
|
+
? {
|
|
324
|
+
action: payload.action,
|
|
325
|
+
channelId: ChannelStore.normalizeChannelId(payload.channelId),
|
|
326
|
+
}
|
|
327
|
+
: await validateCredentialPayload({
|
|
328
|
+
account,
|
|
329
|
+
allowPendingClose: operation === 'broadcast',
|
|
330
|
+
challenge: credential.challenge,
|
|
331
|
+
channelStateTtl,
|
|
332
|
+
chainId: context.chainId,
|
|
333
|
+
client: context.client,
|
|
334
|
+
credentialSource: credential.source,
|
|
335
|
+
escrow: context.escrow,
|
|
336
|
+
expectedOperator: context.methodDetails.operator,
|
|
337
|
+
feePayer: context.feePayer,
|
|
338
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
339
|
+
feeToken: context.methodDetails.feeToken,
|
|
340
|
+
lastOnChainVerified,
|
|
341
|
+
minVoucherDelta: context.minVoucherDelta,
|
|
342
|
+
payload,
|
|
343
|
+
store,
|
|
344
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge,
|
|
345
|
+
});
|
|
288
346
|
return {
|
|
289
347
|
challenge: credential.challenge,
|
|
290
348
|
credential: { ...credential, payload },
|
|
@@ -305,16 +363,46 @@ export function session(p) {
|
|
|
305
363
|
minVoucherDelta: parameters.minVoucherDelta,
|
|
306
364
|
request,
|
|
307
365
|
});
|
|
366
|
+
const chargeAmount = parameters.chargeOnAcceptance &&
|
|
367
|
+
shouldChargeVoucherOnAcceptance({
|
|
368
|
+
capturedRequest: envelope?.capturedRequest,
|
|
369
|
+
payloadAction: payload.action,
|
|
370
|
+
unitType: context.request.unitType,
|
|
371
|
+
})
|
|
372
|
+
? BigInt(context.request.amount ?? challenge.request.amount)
|
|
373
|
+
: undefined;
|
|
374
|
+
const acceptance = {};
|
|
375
|
+
const settleAcceptedCharge = (channel) => maybeSettleScheduled({
|
|
376
|
+
account,
|
|
377
|
+
client: context.client,
|
|
378
|
+
...(context.feePayer ? { feePayer: context.feePayer } : {}),
|
|
379
|
+
feePayerPolicy: parameters.feePayerPolicy,
|
|
380
|
+
feeToken: parameters.feeToken,
|
|
381
|
+
onSessionSettlement,
|
|
382
|
+
onSessionSettlementFailure,
|
|
383
|
+
schedule: settlementSchedule,
|
|
384
|
+
store,
|
|
385
|
+
channel,
|
|
386
|
+
}).catch(ignoreRetryableSettlementFailure);
|
|
308
387
|
const sessionReceipt = await broadcastCredentialPayload({
|
|
309
388
|
account,
|
|
310
389
|
challenge,
|
|
390
|
+
chargeAmount,
|
|
391
|
+
onCharged: (channel) => {
|
|
392
|
+
acceptance.charged = channel;
|
|
393
|
+
},
|
|
311
394
|
channelStateTtl,
|
|
312
395
|
chainId: context.chainId,
|
|
313
396
|
client: context.client,
|
|
314
397
|
credentialSource: credential.source,
|
|
315
398
|
escrow: context.escrow,
|
|
316
399
|
expectedOperator: context.methodDetails.operator,
|
|
317
|
-
feePayer:
|
|
400
|
+
feePayer: await SponsorshipLimit.limitFeePayer({
|
|
401
|
+
chainId: context.chainId,
|
|
402
|
+
feePayer: context.feePayer,
|
|
403
|
+
limit: parameters.sponsorshipLimit,
|
|
404
|
+
payload,
|
|
405
|
+
}),
|
|
318
406
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
319
407
|
feeToken: context.methodDetails.feeToken,
|
|
320
408
|
lastOnChainVerified,
|
|
@@ -328,7 +416,16 @@ export function session(p) {
|
|
|
328
416
|
sseEnabled: Boolean(parameters.sse),
|
|
329
417
|
}),
|
|
330
418
|
store,
|
|
419
|
+
voucherStateMaxAge: parameters.voucherStateMaxAge,
|
|
331
420
|
});
|
|
421
|
+
if (acceptance.charged) {
|
|
422
|
+
const settlementTxHash = await settleAcceptedCharge(acceptance.charged);
|
|
423
|
+
const receipt = {
|
|
424
|
+
...sessionReceipt,
|
|
425
|
+
...(settlementTxHash ? { txHash: settlementTxHash } : {}),
|
|
426
|
+
};
|
|
427
|
+
return parameters.sse ? Transport.markPrepaidSessionTick(receipt) : receipt;
|
|
428
|
+
}
|
|
332
429
|
return applyVerifiedHttpAccounting({
|
|
333
430
|
capturedRequest: envelope?.capturedRequest,
|
|
334
431
|
payloadAction: payload.action,
|
|
@@ -337,18 +434,7 @@ export function session(p) {
|
|
|
337
434
|
sseEnabled: Boolean(parameters.sse),
|
|
338
435
|
markPrepaidReceipt: Transport.markPrepaidSessionTick,
|
|
339
436
|
charge: (channelId, requestAmount) => chargeSessionChannel({ store, channelId, amount: requestAmount }),
|
|
340
|
-
settleCharged:
|
|
341
|
-
account,
|
|
342
|
-
client: context.client,
|
|
343
|
-
...(context.feePayer ? { feePayer: context.feePayer } : {}),
|
|
344
|
-
feePayerPolicy: parameters.feePayerPolicy,
|
|
345
|
-
feeToken: parameters.feeToken,
|
|
346
|
-
onSessionSettlement,
|
|
347
|
-
onSessionSettlementFailure,
|
|
348
|
-
schedule: settlementSchedule,
|
|
349
|
-
store,
|
|
350
|
-
channel,
|
|
351
|
-
}).catch(ignoreRetryableSettlementFailure),
|
|
437
|
+
settleCharged: settleAcceptedCharge,
|
|
352
438
|
});
|
|
353
439
|
};
|
|
354
440
|
const method = Method.toServer(Methods.session, {
|
|
@@ -364,6 +450,7 @@ export function session(p) {
|
|
|
364
450
|
unitType,
|
|
365
451
|
}),
|
|
366
452
|
extensions: { serveWebSocket, settleScheduled },
|
|
453
|
+
...(parameters.reusableChallenges ? { stableBinding: reusableSessionBinding } : {}),
|
|
367
454
|
transport: deriveTransport(transport),
|
|
368
455
|
preflight: parameters.bootstrap
|
|
369
456
|
? async ({ capturedRequest, credential, expires, input, options, realm, secretKey }) => {
|
|
@@ -430,6 +517,7 @@ export function session(p) {
|
|
|
430
517
|
});
|
|
431
518
|
},
|
|
432
519
|
});
|
|
520
|
+
streamVerifiers.set(method, beforeStreamCharge);
|
|
433
521
|
return method;
|
|
434
522
|
}
|
|
435
523
|
(function (session) {
|
|
@@ -444,8 +532,40 @@ export function session(p) {
|
|
|
444
532
|
*
|
|
445
533
|
* Delegates to the shared `deductFromChannel` atomic helper and translates
|
|
446
534
|
* failure modes into typed errors (`InsufficientBalanceError`, `ChannelClosedError`).
|
|
535
|
+
*
|
|
536
|
+
* With `options.idempotencyKey` the channel is charged at most once per key: a retry with the same
|
|
537
|
+
* key and amount returns the channel without charging again, and the same key with a different
|
|
538
|
+
* amount throws `IdempotencyKeyConflictError`.
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* ```ts
|
|
542
|
+
* // One durable job per voucher, retried under its operation ID.
|
|
543
|
+
* const channel = await Session.Server.charge(store, channelId, jobCost, {
|
|
544
|
+
* idempotencyKey: operationId,
|
|
545
|
+
* })
|
|
546
|
+
* ```
|
|
547
|
+
*/
|
|
548
|
+
export async function charge(store, channelId, amount, options) {
|
|
549
|
+
return chargeSessionChannel({ ...options, store: resolveChannelStore(store), channelId, amount });
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Returns an unused amount to a channel's `spent` at most once per idempotency key.
|
|
553
|
+
*
|
|
554
|
+
* The first outcome for a key is final. A credit is `declined` when the channel is closing or
|
|
555
|
+
* finalized, or when it would take `spent` below zero or below what settled, or may still settle,
|
|
556
|
+
* on-chain, because a close captures `max(spent, settled)`; the caller then refunds another way.
|
|
557
|
+
* `unknown` reports a store write that failed and may have committed; retrying with the same key
|
|
558
|
+
* returns the outcome that was recorded.
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts
|
|
562
|
+
* const result = await Session.Server.credit(store, channelId, unused, {
|
|
563
|
+
* idempotencyKey: `refund:${flowId}`,
|
|
564
|
+
* })
|
|
565
|
+
* if (result.status === 'declined') await refundAnotherWay()
|
|
566
|
+
* ```
|
|
447
567
|
*/
|
|
448
|
-
export async function
|
|
449
|
-
return
|
|
568
|
+
export async function credit(store, channelId, amount, options) {
|
|
569
|
+
return ChannelStore.creditChannel(resolveChannelStore(store), channelId, amount, options);
|
|
450
570
|
}
|
|
451
571
|
//# sourceMappingURL=Session.js.map
|
|
@@ -112,8 +112,8 @@ export declare function resolveSettlementSchedule(schedule: SettlementSchedule |
|
|
|
112
112
|
* Computes the schedule progress for an unsettled precompile-backed channel.
|
|
113
113
|
*
|
|
114
114
|
* Returns `undefined` for channels that cannot be scheduled: non-precompile
|
|
115
|
-
* records, channels without an accepted voucher
|
|
116
|
-
*
|
|
115
|
+
* records, or channels without an accepted voucher above the settled amount and
|
|
116
|
+
* within `spent` (see {@link ChannelStore.resolveSettleableVoucher}).
|
|
117
117
|
*/
|
|
118
118
|
export declare function resolveSettlementProgress(channel: ChannelStore.State): SettlementProgress | undefined;
|
|
119
119
|
/** Returns whether the precompile channel has crossed any configured settlement threshold. */
|
|
@@ -145,7 +145,7 @@ export type ChargeSessionChannel = (channelId: Hex, amount: bigint) => Promise<C
|
|
|
145
145
|
/** Callback used by post-verification accounting to run server-owned settlement policy. */
|
|
146
146
|
export type SettleChargedSessionChannel = (channel: ChannelStore.State) => Promise<Hex | undefined>;
|
|
147
147
|
/** Inputs for charging a precompile-backed session channel. */
|
|
148
|
-
export type ChargeParameters = {
|
|
148
|
+
export type ChargeParameters = ChannelStore.ChargeOptions & {
|
|
149
149
|
/** Server-side channel store. */
|
|
150
150
|
store: ChannelStore.ChannelStore;
|
|
151
151
|
/** Channel ID to deduct from. */
|
|
@@ -174,6 +174,15 @@ export type ApplyVerifiedHttpAccountingParameters = {
|
|
|
174
174
|
};
|
|
175
175
|
/** Returns whether an accepted credential will authorize and charge an HTTP content response. */
|
|
176
176
|
export declare function shouldApplyVerifiedHttpAccounting(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction' | 'sseEnabled'>): boolean;
|
|
177
|
+
/**
|
|
178
|
+
* Returns whether a voucher pays for its request in the store update that accepts it. A
|
|
179
|
+
* request-priced content request is charged exactly once, so this is the charge the HTTP
|
|
180
|
+
* accounting or the streamed response would make, without a second store update; the receipt is
|
|
181
|
+
* then marked prepaid so an SSE response does not charge again.
|
|
182
|
+
*/
|
|
183
|
+
export declare function shouldChargeVoucherOnAcceptance(parameters: Pick<ApplyVerifiedHttpAccountingParameters, 'capturedRequest' | 'payloadAction'> & {
|
|
184
|
+
unitType: unknown;
|
|
185
|
+
}): boolean;
|
|
177
186
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
178
187
|
export declare function applyVerifiedHttpAccounting(parameters: ApplyVerifiedHttpAccountingParameters): Promise<SessionReceipt>;
|
|
179
188
|
/** Atomically deducts spend from a channel and maps store failures to typed session errors. */
|
|
@@ -238,7 +247,12 @@ export declare function getClientAccount(client: {
|
|
|
238
247
|
export declare function assertSettlementSender(parameters: SettlementSenderParameters): void;
|
|
239
248
|
/** Applies automatic settlement when the server-owned schedule is due. */
|
|
240
249
|
export declare function maybeSettleScheduled(parameters: MaybeSettleScheduledParameters): Promise<Hex | undefined>;
|
|
241
|
-
/**
|
|
250
|
+
/**
|
|
251
|
+
* Settles the largest accepted voucher within the channel's `spent` for a precompile-backed session
|
|
252
|
+
* channel (see {@link ChannelStore.resolveSettleableVoucher}). The escrow captures the voucher's
|
|
253
|
+
* whole cumulative amount, so a voucher above `spent`, such as the highest one after a credit, is
|
|
254
|
+
* never settled. Until the transaction can no longer land, credits keep `spent` at or above it.
|
|
255
|
+
*/
|
|
242
256
|
export declare function settle(store_: SessionStoreInput, client: Chain.TransactionClient, channelId_: Hex, options?: SettlementTransactionOptions): Promise<Hex>;
|
|
243
257
|
/** Raised when a settlement transaction confirmed but the channel store could not record it. */
|
|
244
258
|
export declare class SettlementCheckpointError extends Error {
|