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
|
@@ -67,15 +67,14 @@ export function resolveSettlementSchedule(schedule, decimals) {
|
|
|
67
67
|
* Computes the schedule progress for an unsettled precompile-backed channel.
|
|
68
68
|
*
|
|
69
69
|
* Returns `undefined` for channels that cannot be scheduled: non-precompile
|
|
70
|
-
* records, channels without an accepted voucher
|
|
71
|
-
*
|
|
70
|
+
* records, or channels without an accepted voucher above the settled amount and
|
|
71
|
+
* within `spent` (see {@link ChannelStore.resolveSettleableVoucher}).
|
|
72
72
|
*/
|
|
73
73
|
export function resolveSettlementProgress(channel) {
|
|
74
74
|
if (!ChannelStore.isPrecompileState(channel))
|
|
75
75
|
return undefined;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (channel.highestVoucher.cumulativeAmount <= channel.settledOnChain)
|
|
76
|
+
// A settlement captures a voucher whole, so only a voucher within spend can be scheduled.
|
|
77
|
+
if (!ChannelStore.resolveSettleableVoucher(channel))
|
|
79
78
|
return undefined;
|
|
80
79
|
const amountBoundary = channel.lastSettlementSpent ?? channel.settledOnChain;
|
|
81
80
|
const timestampBoundary = Date.parse(channel.lastSettlementAt ?? channel.createdAt);
|
|
@@ -171,6 +170,18 @@ export function shouldApplyVerifiedHttpAccounting(parameters) {
|
|
|
171
170
|
return false;
|
|
172
171
|
return isSessionContentRequest(capturedRequest);
|
|
173
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Returns whether a voucher pays for its request in the store update that accepts it. A
|
|
175
|
+
* request-priced content request is charged exactly once, so this is the charge the HTTP
|
|
176
|
+
* accounting or the streamed response would make, without a second store update; the receipt is
|
|
177
|
+
* then marked prepaid so an SSE response does not charge again.
|
|
178
|
+
*/
|
|
179
|
+
export function shouldChargeVoucherOnAcceptance(parameters) {
|
|
180
|
+
const { capturedRequest, payloadAction, unitType } = parameters;
|
|
181
|
+
if (!capturedRequest || payloadAction !== 'voucher' || unitType !== 'request')
|
|
182
|
+
return false;
|
|
183
|
+
return isSessionContentRequest(capturedRequest);
|
|
184
|
+
}
|
|
174
185
|
/** Applies the default HTTP content charge after a session credential has been accepted. */
|
|
175
186
|
export async function applyVerifiedHttpAccounting(parameters) {
|
|
176
187
|
const { receipt, sseEnabled } = parameters;
|
|
@@ -191,12 +202,18 @@ export async function applyVerifiedHttpAccounting(parameters) {
|
|
|
191
202
|
}
|
|
192
203
|
/** Atomically deducts spend from a channel and maps store failures to typed session errors. */
|
|
193
204
|
export async function chargeSessionChannel(parameters) {
|
|
194
|
-
const { store, channelId, amount } = parameters;
|
|
205
|
+
const { store, channelId, amount, idempotencyKey, retention } = parameters;
|
|
206
|
+
ChannelStore.assertIdempotencyOptions({ idempotencyKey, retention });
|
|
195
207
|
let result;
|
|
196
208
|
try {
|
|
197
|
-
result = await ChannelStore.deductFromChannel(store, channelId, amount
|
|
209
|
+
result = await ChannelStore.deductFromChannel(store, channelId, amount, {
|
|
210
|
+
idempotencyKey,
|
|
211
|
+
retention,
|
|
212
|
+
});
|
|
198
213
|
}
|
|
199
|
-
catch {
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof ChannelStore.IdempotencyKeyConflictError)
|
|
216
|
+
throw error;
|
|
200
217
|
throw new ChannelClosedError({ reason: 'channel not found' });
|
|
201
218
|
}
|
|
202
219
|
if (!result.ok) {
|
|
@@ -286,7 +303,12 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
286
303
|
await markSettlementComplete({ channelId: channel.channelId, leaseOwner, store }).catch(release);
|
|
287
304
|
return txHash;
|
|
288
305
|
}
|
|
289
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Settles the largest accepted voucher within the channel's `spent` for a precompile-backed session
|
|
308
|
+
* channel (see {@link ChannelStore.resolveSettleableVoucher}). The escrow captures the voucher's
|
|
309
|
+
* whole cumulative amount, so a voucher above `spent`, such as the highest one after a credit, is
|
|
310
|
+
* never settled. Until the transaction can no longer land, credits keep `spent` at or above it.
|
|
311
|
+
*/
|
|
290
312
|
export async function settle(store_, client, channelId_, options) {
|
|
291
313
|
const store = resolveChannelStore(store_);
|
|
292
314
|
const channelId = ChannelStore.normalizeChannelId(channelId_);
|
|
@@ -306,20 +328,72 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
306
328
|
payee: channel.payee,
|
|
307
329
|
sender: account?.address,
|
|
308
330
|
});
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
331
|
+
// Pin the transaction's expiry so the credit floor below can expire once it can no longer land.
|
|
332
|
+
const validBefore = Math.floor(Date.now() / 1000) + ChannelStore.pendingCloseValiditySeconds;
|
|
333
|
+
const pending = {
|
|
334
|
+
amount: 0n,
|
|
335
|
+
expiresAt: (validBefore + ChannelStore.pendingCloseClockSkewSeconds) * 1000,
|
|
336
|
+
};
|
|
337
|
+
const claim = {};
|
|
338
|
+
// Choosing the voucher and raising the credit floor in one update keeps a concurrent credit from
|
|
339
|
+
// lowering spent below what this transaction captures.
|
|
340
|
+
await store.updateChannel(channelId, (current) => {
|
|
341
|
+
claim.channel = current ?? undefined;
|
|
342
|
+
claim.voucher = (current && ChannelStore.resolveSettleableVoucher(current)) ?? undefined;
|
|
343
|
+
if (!current || !claim.voucher)
|
|
344
|
+
return current;
|
|
345
|
+
pending.amount = claim.voucher.cumulativeAmount;
|
|
346
|
+
const live = current.pendingSettlement && current.pendingSettlement.expiresAt > Date.now()
|
|
347
|
+
? current.pendingSettlement
|
|
348
|
+
: undefined;
|
|
349
|
+
return {
|
|
350
|
+
...current,
|
|
351
|
+
pendingSettlement: {
|
|
352
|
+
amount: ChannelStore.keepGreater(live?.amount ?? 0n, pending.amount),
|
|
353
|
+
expiresAt: Math.max(live?.expiresAt ?? 0, pending.expiresAt),
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
const { voucher } = claim;
|
|
358
|
+
if (!voucher || !claim.channel)
|
|
359
|
+
throw new VerificationFailedError({
|
|
360
|
+
reason: 'nothing to settle: no accepted voucher above the settled amount is within spent',
|
|
361
|
+
});
|
|
362
|
+
const claimed = claim.channel;
|
|
363
|
+
const amount = uint96(voucher.cumulativeAmount);
|
|
364
|
+
const { signature } = voucher;
|
|
365
|
+
// A revert proves the transaction will not land, so its credit floor can go at once.
|
|
366
|
+
const release = () => store
|
|
367
|
+
.updateChannel(channelId, (current) => {
|
|
368
|
+
if (current?.pendingSettlement?.amount !== pending.amount ||
|
|
369
|
+
current.pendingSettlement.expiresAt !== pending.expiresAt)
|
|
370
|
+
return current;
|
|
371
|
+
const { pendingSettlement: _, ...rest } = current;
|
|
372
|
+
return rest;
|
|
373
|
+
})
|
|
374
|
+
.catch(() => undefined);
|
|
375
|
+
let txHash;
|
|
376
|
+
let receipt;
|
|
377
|
+
try {
|
|
378
|
+
// A replica behind the open or top-up that admitted this voucher rejects the settle while
|
|
379
|
+
// preparing it (e.g. `ChannelNotFound`); nothing was broadcast, so retry until it catches up.
|
|
380
|
+
txHash = await Chain.readbackWithRetry(() => Chain.settleOnChain(client, channel.descriptor, amount, signature, escrow, account
|
|
381
|
+
? {
|
|
382
|
+
account,
|
|
383
|
+
...(options?.feePayer ? { feePayer: options.feePayer } : {}),
|
|
384
|
+
...(options?.feePayerPolicy ? { feePayerPolicy: options.feePayerPolicy } : {}),
|
|
385
|
+
...(options?.feeToken ? { feeToken: options.feeToken } : {}),
|
|
386
|
+
candidateFeeTokens: options?.candidateFeeTokens ?? [channel.token],
|
|
387
|
+
validBefore,
|
|
388
|
+
}
|
|
389
|
+
: { validBefore }), { shouldRetry: isExecutionRevert });
|
|
390
|
+
receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (isExecutionRevert(error) || error instanceof VerificationFailedError)
|
|
394
|
+
await release();
|
|
395
|
+
throw error;
|
|
396
|
+
}
|
|
323
397
|
const settled = readSettledReceiptFields(Chain.getChannelEvent(receipt, 'Settled', channelId));
|
|
324
398
|
const { deltaPaid, newSettled } = settled;
|
|
325
399
|
if (newSettled < amount)
|
|
@@ -332,16 +406,29 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
332
406
|
});
|
|
333
407
|
let checkpointError;
|
|
334
408
|
await store
|
|
335
|
-
.updateChannel(channelId, (current) =>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
409
|
+
.updateChannel(channelId, (current) => {
|
|
410
|
+
if (!current)
|
|
411
|
+
return current;
|
|
412
|
+
const settledOnChain = ChannelStore.keepGreater(current.settledOnChain, newSettled);
|
|
413
|
+
const { pendingSettlement, ...rest } = current;
|
|
414
|
+
return {
|
|
415
|
+
...rest,
|
|
416
|
+
// The recorded settlement now keeps spent at or above what this transaction captured.
|
|
417
|
+
...(pendingSettlement && pendingSettlement.amount > settledOnChain
|
|
418
|
+
? { pendingSettlement }
|
|
419
|
+
: {}),
|
|
420
|
+
...(current.retainedVouchers
|
|
421
|
+
? {
|
|
422
|
+
retainedVouchers: current.retainedVouchers.filter((retained) => retained.cumulativeAmount > settledOnChain),
|
|
423
|
+
}
|
|
424
|
+
: {}),
|
|
425
|
+
settledOnChain,
|
|
339
426
|
lastSettlementAt: new Date().toISOString(),
|
|
340
|
-
// Charges accepted after the voucher was
|
|
341
|
-
lastSettlementSpent: ChannelStore.keepGreater(current.lastSettlementSpent ?? 0n,
|
|
342
|
-
lastSettlementUnits: Math.max(current.lastSettlementUnits ?? 0,
|
|
343
|
-
}
|
|
344
|
-
|
|
427
|
+
// Charges accepted after the voucher was chosen are not covered by this transaction.
|
|
428
|
+
lastSettlementSpent: ChannelStore.keepGreater(current.lastSettlementSpent ?? 0n, amount),
|
|
429
|
+
lastSettlementUnits: Math.max(current.lastSettlementUnits ?? 0, claimed.units),
|
|
430
|
+
};
|
|
431
|
+
})
|
|
345
432
|
.catch((cause) => {
|
|
346
433
|
checkpointError = { cause };
|
|
347
434
|
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Address } from 'ox';
|
|
2
|
+
import type * as Store from '../../../Store.js';
|
|
3
|
+
import type { SessionCredentialPayload } from '../precompile/Protocol.js';
|
|
4
|
+
/**
|
|
5
|
+
* Sponsored opens, and separately sponsored top-ups, each verified payer may make per UTC day. It is
|
|
6
|
+
* tempoxyz/mpp-proxy's `DAILY_CHANNEL_OPEN_LIMIT`, the limit its proxy applied to sponsored channel
|
|
7
|
+
* opens before the limit moved into mppx.
|
|
8
|
+
*/
|
|
9
|
+
export declare const defaultPerDay = 5;
|
|
10
|
+
/** Limits on fee-payer-sponsored channel opens and top-ups. */
|
|
11
|
+
export type Parameters = {
|
|
12
|
+
/** Atomic store holding one usage record per payer and chain. */
|
|
13
|
+
store: Store.AtomicStore;
|
|
14
|
+
/** Sponsored opens, and separately sponsored top-ups, per payer per UTC day. @default 5 */
|
|
15
|
+
perDay?: number | undefined;
|
|
16
|
+
};
|
|
17
|
+
/** Validates a configured limit. */
|
|
18
|
+
export declare function assertParameters(parameters: Parameters | undefined): void;
|
|
19
|
+
/**
|
|
20
|
+
* Returns the verified signer of a serialized Tempo transaction, lowercased, or null. A
|
|
21
|
+
* credential's `source` is client-supplied and unverified at this point, and so are the 0x78
|
|
22
|
+
* explicit sender slot and a keychain signature's account, so limits key on what the signatures
|
|
23
|
+
* prove:
|
|
24
|
+
* - a direct signature: its signer;
|
|
25
|
+
* - an access key authorized in the same transaction: the account that signed the authorization,
|
|
26
|
+
* so fresh keys do not get a fresh quota;
|
|
27
|
+
* - an access key authorized earlier: the key itself (its account link is only checked on chain).
|
|
28
|
+
*/
|
|
29
|
+
export declare function verifiedPayer(serializedTransaction: string): Address.Address | null;
|
|
30
|
+
/**
|
|
31
|
+
* Records one sponsored `action` for `payer` on the current UTC day. Returns false, recording
|
|
32
|
+
* nothing, once the payer has used `perDay` of them.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reserve(store: Store.AtomicStore, parameters: {
|
|
35
|
+
action: 'open' | 'topUp';
|
|
36
|
+
chainId: number;
|
|
37
|
+
payer: string;
|
|
38
|
+
perDay?: number | undefined;
|
|
39
|
+
now?: Date | undefined;
|
|
40
|
+
}): Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Returns `feePayer` when the credential's open or top-up may be sponsored, and `undefined` once
|
|
43
|
+
* its verified payer used the day's sponsored transactions, or when no signature proves a payer.
|
|
44
|
+
* The transaction is then broadcast as the payer signed it, paying its own fees.
|
|
45
|
+
*/
|
|
46
|
+
export declare function limitFeePayer<feePayer>(parameters: {
|
|
47
|
+
chainId: number;
|
|
48
|
+
feePayer: feePayer | undefined;
|
|
49
|
+
limit: Parameters | undefined;
|
|
50
|
+
payload: SessionCredentialPayload;
|
|
51
|
+
}): Promise<feePayer | undefined>;
|
|
52
|
+
//# sourceMappingURL=SponsorshipLimit.d.ts.map
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Address } from 'ox';
|
|
2
|
+
import { KeyAuthorization, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo';
|
|
3
|
+
/**
|
|
4
|
+
* Sponsored opens, and separately sponsored top-ups, each verified payer may make per UTC day. It is
|
|
5
|
+
* tempoxyz/mpp-proxy's `DAILY_CHANNEL_OPEN_LIMIT`, the limit its proxy applied to sponsored channel
|
|
6
|
+
* opens before the limit moved into mppx.
|
|
7
|
+
*/
|
|
8
|
+
export const defaultPerDay = 5;
|
|
9
|
+
/** Validates a configured limit. */
|
|
10
|
+
export function assertParameters(parameters) {
|
|
11
|
+
const perDay = parameters?.perDay;
|
|
12
|
+
if (perDay !== undefined && (!Number.isSafeInteger(perDay) || perDay < 0))
|
|
13
|
+
throw new Error('`sponsorshipLimit.perDay` must be a non-negative integer.');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The address that provably produced `signature` over `payload`, or null. secp256k1 recovery is
|
|
17
|
+
* inherently verified; P256 and WebAuthn envelopes carry a public key, so their signature is
|
|
18
|
+
* checked against it. Multisig has no single signer.
|
|
19
|
+
*/
|
|
20
|
+
function verifiedSigner(payload, signature) {
|
|
21
|
+
if (signature.type === 'secp256k1')
|
|
22
|
+
return SignatureEnvelope.extractAddress({ payload, signature });
|
|
23
|
+
if (signature.type === 'p256' || signature.type === 'webAuthn') {
|
|
24
|
+
const { publicKey } = signature;
|
|
25
|
+
return SignatureEnvelope.verify(signature, { payload, publicKey })
|
|
26
|
+
? Address.fromPublicKey(publicKey)
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Returns the verified signer of a serialized Tempo transaction, lowercased, or null. A
|
|
33
|
+
* credential's `source` is client-supplied and unverified at this point, and so are the 0x78
|
|
34
|
+
* explicit sender slot and a keychain signature's account, so limits key on what the signatures
|
|
35
|
+
* prove:
|
|
36
|
+
* - a direct signature: its signer;
|
|
37
|
+
* - an access key authorized in the same transaction: the account that signed the authorization,
|
|
38
|
+
* so fresh keys do not get a fresh quota;
|
|
39
|
+
* - an access key authorized earlier: the key itself (its account link is only checked on chain).
|
|
40
|
+
*/
|
|
41
|
+
export function verifiedPayer(serializedTransaction) {
|
|
42
|
+
if (!/^0x7[68][0-9a-fA-F]+$/.test(serializedTransaction))
|
|
43
|
+
return null;
|
|
44
|
+
try {
|
|
45
|
+
const { from: _claimedSender, ...fields } = TxEnvelopeTempo.deserialize(serializedTransaction);
|
|
46
|
+
const { signature } = fields;
|
|
47
|
+
if (!signature)
|
|
48
|
+
return null;
|
|
49
|
+
const envelope = TxEnvelopeTempo.from(fields);
|
|
50
|
+
if (signature.type !== 'keychain')
|
|
51
|
+
return lower(verifiedSigner(TxEnvelopeTempo.getSignPayload(envelope), signature));
|
|
52
|
+
// Keychain V2 signs over the account too: keccak256(0x04 || sigHash || userAddress).
|
|
53
|
+
const accessKey = verifiedSigner(TxEnvelopeTempo.getSignPayload(envelope, signature.version === 'v2' ? { from: signature.userAddress } : {}), signature.inner);
|
|
54
|
+
if (!accessKey)
|
|
55
|
+
return null;
|
|
56
|
+
const authorization = fields.keyAuthorization;
|
|
57
|
+
if (!authorization)
|
|
58
|
+
return lower(accessKey);
|
|
59
|
+
if (!authorization.signature || !Address.isEqual(authorization.address, accessKey))
|
|
60
|
+
return null;
|
|
61
|
+
return lower(verifiedSigner(KeyAuthorization.getSignPayload(authorization), authorization.signature));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function lower(address) {
|
|
68
|
+
return address ? address.toLowerCase() : null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Records one sponsored `action` for `payer` on the current UTC day. Returns false, recording
|
|
72
|
+
* nothing, once the payer has used `perDay` of them.
|
|
73
|
+
*/
|
|
74
|
+
export async function reserve(store, parameters) {
|
|
75
|
+
const { action, chainId, payer, perDay = defaultPerDay } = parameters;
|
|
76
|
+
const day = (parameters.now ?? new Date()).toISOString().slice(0, 10);
|
|
77
|
+
const key = `mppx:session:sponsorship:${chainId}:${payer.toLowerCase()}`;
|
|
78
|
+
return store.update(key, (current) => {
|
|
79
|
+
const usage = current?.day === day ? current : { day, open: 0, topUp: 0 };
|
|
80
|
+
if (usage[action] >= perDay)
|
|
81
|
+
return { op: 'noop', result: false };
|
|
82
|
+
return { op: 'set', value: { ...usage, [action]: usage[action] + 1 }, result: true };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns `feePayer` when the credential's open or top-up may be sponsored, and `undefined` once
|
|
87
|
+
* its verified payer used the day's sponsored transactions, or when no signature proves a payer.
|
|
88
|
+
* The transaction is then broadcast as the payer signed it, paying its own fees.
|
|
89
|
+
*/
|
|
90
|
+
export async function limitFeePayer(parameters) {
|
|
91
|
+
const { chainId, feePayer, limit, payload } = parameters;
|
|
92
|
+
if (!feePayer || !limit)
|
|
93
|
+
return feePayer;
|
|
94
|
+
if (payload.action !== 'open' && payload.action !== 'topUp')
|
|
95
|
+
return feePayer;
|
|
96
|
+
const payer = verifiedPayer(payload.transaction);
|
|
97
|
+
if (!payer)
|
|
98
|
+
return undefined;
|
|
99
|
+
const allowed = await reserve(limit.store, {
|
|
100
|
+
action: payload.action,
|
|
101
|
+
chainId,
|
|
102
|
+
payer,
|
|
103
|
+
perDay: limit.perDay,
|
|
104
|
+
});
|
|
105
|
+
return allowed ? feePayer : undefined;
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=SponsorshipLimit.js.map
|
|
@@ -27,9 +27,8 @@ export type { SessionController } from './MeteredStream.js';
|
|
|
27
27
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
28
28
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
29
29
|
* and polls store until the client tops up the channel.
|
|
30
|
-
* 4. Commits
|
|
31
|
-
*
|
|
32
|
-
* commits that terminal charge on successful completion.
|
|
30
|
+
* 4. Commits automatic charges before emission and manual charges before
|
|
31
|
+
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
33
32
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
34
33
|
*
|
|
35
34
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -43,6 +42,8 @@ export declare namespace serve {
|
|
|
43
42
|
challengeId: string;
|
|
44
43
|
tickCost: bigint;
|
|
45
44
|
generate: AsyncIterable<string> | ((stream: SessionController) => AsyncIterable<string>);
|
|
45
|
+
/** Verifies live escrow before charging and delivering stream items. */
|
|
46
|
+
beforeCharge?: MeteredStreamOptions['beforeCharge'];
|
|
46
47
|
/** Callback invoked after each nonzero stream charge is committed. */
|
|
47
48
|
onChargeCommitted?: MeteredStreamOptions['onChargeCommitted'];
|
|
48
49
|
pollIntervalMs?: number | undefined;
|
|
@@ -26,9 +26,8 @@ export { extractData, formatMessageEvent, formatNeedVoucherEvent, formatReceiptE
|
|
|
26
26
|
* 2. If balance is sufficient, emits `event: message` with the value.
|
|
27
27
|
* 3. If balance is exhausted, emits `event: payment-need-voucher`
|
|
28
28
|
* and polls store until the client tops up the channel.
|
|
29
|
-
* 4. Commits
|
|
30
|
-
*
|
|
31
|
-
* commits that terminal charge on successful completion.
|
|
29
|
+
* 4. Commits automatic charges before emission and manual charges before
|
|
30
|
+
* `stream.charge()` resolves, retaining them on cancellation or failure.
|
|
32
31
|
* 5. On generator completion, emits a final `event: payment-receipt`.
|
|
33
32
|
*
|
|
34
33
|
* Returns a `ReadableStream<Uint8Array>` suitable for use as an HTTP response body.
|
|
@@ -44,6 +43,7 @@ export function serve(options) {
|
|
|
44
43
|
channelId,
|
|
45
44
|
tickCost,
|
|
46
45
|
generate,
|
|
46
|
+
beforeCharge: options.beforeCharge,
|
|
47
47
|
onChargeCommitted: options.onChargeCommitted,
|
|
48
48
|
pollIntervalMs,
|
|
49
49
|
prepaidUnits: options.prepaidUnits,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SessionCredentialContext } from '../precompile/Protocol.js';
|
|
1
2
|
import * as ChannelStore from './ChannelStore.js';
|
|
2
3
|
import type { MeteredStreamOptions, SessionController } from './MeteredStream.js';
|
|
3
4
|
import type { SettleChargedSessionChannel } from './Settlement.js';
|
|
@@ -16,6 +17,11 @@ export type SessionRouteResult = {
|
|
|
16
17
|
};
|
|
17
18
|
/** HTTP route used to perform a WebSocket payment probe. */
|
|
18
19
|
export type SessionRoute = (request: Request) => Promise<SessionRouteResult>;
|
|
20
|
+
type StreamContext = {
|
|
21
|
+
challengeId: string;
|
|
22
|
+
channelId: SessionCredentialContext['channelId'];
|
|
23
|
+
tickCost: bigint;
|
|
24
|
+
};
|
|
19
25
|
/**
|
|
20
26
|
* Bridge a WebSocket connection to a Tempo session payment flow.
|
|
21
27
|
*
|
|
@@ -47,7 +53,9 @@ export declare namespace serve {
|
|
|
47
53
|
/** Payment route handler. Receives synthetic `POST` requests with only
|
|
48
54
|
* the `Authorization` header — no cookies, bodies, or upgrade headers. */
|
|
49
55
|
route: SessionRoute;
|
|
50
|
-
/**
|
|
56
|
+
/** Verifies live escrow before charging and delivering stream items. */
|
|
57
|
+
beforeCharge?: ((channelId: StreamContext['channelId']) => void | Promise<void>) | undefined;
|
|
58
|
+
/** Callback invoked after each nonzero stream charge is committed. */
|
|
51
59
|
onChargeCommitted?: MeteredStreamOptions['onChargeCommitted'];
|
|
52
60
|
/** @deprecated Use `onChargeCommitted`. */
|
|
53
61
|
settleScheduled?: SettleChargedSessionChannel | undefined;
|
|
@@ -65,6 +65,7 @@ export async function serve(options) {
|
|
|
65
65
|
channelId: context.channelId,
|
|
66
66
|
tickCost: context.tickCost,
|
|
67
67
|
generate,
|
|
68
|
+
beforeCharge: () => options.beforeCharge?.(context.channelId),
|
|
68
69
|
onChargeCommitted: options.onChargeCommitted ?? options.settleScheduled,
|
|
69
70
|
pollIntervalMs,
|
|
70
71
|
signal: abortController.signal,
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
export { charge, session, settle, settleBatch } from './Session.js';
|
|
1
|
+
export { charge, credit, session, settle, settleBatch } from './Session.js';
|
|
2
|
+
/** Close supervision for payer close requests on idle channels. */
|
|
3
|
+
export { superviseCloses } from './CloseSupervisor.js';
|
|
4
|
+
export type { CloseSupervision, CloseSupervisionOutcome, SuperviseClosesParameters, } from './CloseSupervisor.js';
|
|
5
|
+
/** Idempotent charge and credit options and outcomes, and the voucher a settlement captures. */
|
|
6
|
+
export { IdempotencyKeyConflictError, resolveSettleableVoucher } from './ChannelStore.js';
|
|
7
|
+
export type { ChargeOptions, CreditDeclineReason, CreditOptions, CreditResult, State as ChannelState, } from './ChannelStore.js';
|
|
8
|
+
/** Limits on fee-payer-sponsored channel opens and top-ups. */
|
|
9
|
+
export type { Parameters as SponsorshipLimitParameters } from './SponsorshipLimit.js';
|
|
2
10
|
/** SSE helpers and types for Tempo session streams. */
|
|
3
11
|
export * as Sse from './Sse.js';
|
|
4
12
|
/** Server-side automatic settlement schedule. */
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
export { charge, session, settle, settleBatch } from './Session.js';
|
|
1
|
+
export { charge, credit, session, settle, settleBatch } from './Session.js';
|
|
2
|
+
/** Close supervision for payer close requests on idle channels. */
|
|
3
|
+
export { superviseCloses } from './CloseSupervisor.js';
|
|
4
|
+
/** Idempotent charge and credit options and outcomes, and the voucher a settlement captures. */
|
|
5
|
+
export { IdempotencyKeyConflictError, resolveSettleableVoucher } from './ChannelStore.js';
|
|
2
6
|
/** SSE helpers and types for Tempo session streams. */
|
|
3
7
|
export * as Sse from './Sse.js';
|
|
4
8
|
//# sourceMappingURL=index.js.map
|