mppx 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/cli/cli.js +1 -1
- package/dist/internal/version.d.ts +2 -2
- package/dist/internal/version.js +1 -1
- package/dist/tempo/server/Methods.js +3 -1
- package/dist/tempo/server/index.d.ts +1 -1
- package/dist/tempo/session/precompile/Chain.d.ts +2 -0
- package/dist/tempo/session/precompile/Chain.js +7 -2
- package/dist/tempo/session/server/ChannelStore.d.ts +8 -0
- package/dist/tempo/session/server/ChannelStore.js +17 -0
- package/dist/tempo/session/server/CredentialVerification.d.ts +3 -1
- package/dist/tempo/session/server/CredentialVerification.js +56 -11
- package/dist/tempo/session/server/Session.d.ts +4 -2
- package/dist/tempo/session/server/Session.js +21 -5
- package/dist/tempo/session/server/Settlement.d.ts +33 -1
- package/dist/tempo/session/server/Settlement.js +85 -31
- package/dist/tempo/session/server/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# mppx
|
|
2
2
|
|
|
3
|
+
## 0.13.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 06dc781: Fixed `tempo/session` reopening a channel when a broadcast close failed ambiguously. The pending-close marker stayed until the close transaction expired, and a later voucher or close cleared it once the chain showed the channel still open.
|
|
8
|
+
- 55c3c26: Changed `tempo/session` to serve a charged request when its scheduled settlement hit a transport or RPC failure; the next settlement collected the charge. Reverts and configuration errors still failed the request.
|
|
9
|
+
- c22cf1a: Fixed `tempo/session` failing a charged request when its settlement confirmed but the channel store could not record it. `settle` raised `SettlementCheckpointError` with the transaction hash instead.
|
|
10
|
+
- 6921631: Fixed scheduled `tempo/session` settlement marking charges accepted while the settlement transaction was pending as settled. `settle` recorded the spend and units read with the submitted voucher instead.
|
|
11
|
+
- 55c3c26: Added `onSessionSettlementFailure` to `tempo.session()`, called with the chain, channel, error and trigger (`scheduled` or `close`) when a scheduled settlement or close transaction fails.
|
|
12
|
+
|
|
3
13
|
## 0.13.2
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/dist/cli/cli.js
CHANGED
|
@@ -242,7 +242,7 @@ $&`).replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g,`$1$2`).replace(/\
|
|
|
242
242
|
`)){let e=t.trim();if(e===`[[keys]]`){r?.wallet_address&&n.push(r),r={wallet_type:`local`,wallet_address:``,chain_id:0};continue}if(!r)continue;let i=e.match(/^(\w+)\s*=\s*"?([^"]*)"?$/);if(!i)continue;let[,a,o]=i;a===`wallet_type`?r.wallet_type=o:a===`wallet_address`?r.wallet_address=o:a===`chain_id`&&(r.chain_id=Number.parseInt(o,10))}return r?.wallet_address&&n.push(r),n}catch{return[]}}function rP(e){let t=nP();if(t.length===0)return;let n=e.slice(6);if(n==="default"||n===``)return t[0];let r=Number.parseInt(n,10);if(!Number.isNaN(r)&&r>=0&&r<t.length)return t[r]}function iP(){if(fP!==void 0)return fP;try{m.execFileSync(`which`,[`tempo`],{stdio:`ignore`}),fP=!0}catch{fP=!1}return fP}async function aP(e){return new Promise((t,n)=>{m.execFile(`tempo`,[`mpp`,`sign`,`--challenge`,e],(e,r,i)=>{if(e){let t=i?.trim()||e.message;n(Error(`tempo mpp sign failed: ${t}`));return}let a=r.trim();if(!a){n(Error(`tempo mpp sign returned empty output`));return}t(a)})})}function oP(){let e=UO();if(yO(e.get())&&!iP()&&b.platform()===`darwin`)try{let t=m.execFileSync(`security`,[`dump-keychain`],{encoding:`utf-8`}),n=[];for(let e of t.split(`keychain:`)){let t=e.match(/"svce"<blob>="([^"]*)"/),r=e.match(/"acct"<blob>="([^"]*)"/);t?.[1]===sP&&r?.[1]&&n.push(r[1])}if(n.length>0)return e.set(n[0]),n[0]}catch{}}var sP,cP,lP,uP,dP,fP,pP=F((()=>{Dx(),oT(),Px(),eC(),NN(),ME(),Ej(),ZO(),LO(),Xk(),sP=`mppx`,cP=p.union([p.boolean(),p.literal(`true`).transform(()=>!0),p.literal(`false`).transform(()=>!1)]),lP=p.object({allowCustomEscrow:p.optional(cP),autoSwap:p.optional(cP),channel:p.optional(p.coerce.string()),deposit:p.optional(p.union([p.string(),p.number()])),payWith:p.optional(p.string()),slippage:p.optional(p.coerce.number()),tokenIn:p.optional(p.string())}),uP=[`allowCustomEscrow`,`autoSwap`,`channel`,`deposit`,`payWith`,`slippage`,`tokenIn`],dP=[`payable`,`unknownBalance`,`insufficientBalance`,`unsupported`]}));ES(),ZO(),Xk();const mP=[...Object.entries(rk),...Object.entries(ik),...Object.entries(ak),...Object.entries(ok)];function hP(){return Yk({method:`evm`,supports:gP,async setup({challenge:e,options:t}){let n=e.request,r=n.network?_P(pk(n.network)):void 0,i=n.network&&n.asset?mP.find(([,e])=>dk(e,n.asset,n.network)):void 0,a=n.extra?.name,o=n.extra?.version,s=n.asset&&n.network&&typeof a==`string`&&typeof o==`string`?tk({address:n.asset,decimals:i?.[1].decimals??6,network:n.network,transfer:{name:a,type:`eip3009`,version:o}}):void 0,c=await JO(t.account);return{explorerUrl:r?.blockExplorers?.default?.url,methods:[...Jk({account:c,...s&&{currencies:[s]}})],tokenDecimals:i?.[1].decimals??6,tokenSymbol:i?.[0]??``}}})}function gP(e){if(!wS(e))return!1;let t=e.request,n=t.extra,r=n?.assetTransferMethod;return t.scheme===`exact`&&typeof t.asset==`string`&&typeof t.network==`string`&&typeof n?.name==`string`&&typeof n?.version==`string`&&(r===void 0||r===`eip3009`)}function _P(e){return Object.values(Be).find(t=>t.id===e)}gS(),KS(),pP();const vP=[LN(),oA(),Zk(),hP()];async function yP(e,t,n){let r=n;for(let n of t??[]){let t=await n.preparePayment?.({challenge:e,...r===void 0?{}:{credentialContext:r}});t&&`credentialContext`in t&&(r=t.credentialContext)}return r}function bP(e,t){let n=t?.plugins?.find(t=>wP(t,e));if(n)return{plugin:n};let r=CP(t)?.filter(t=>t.name===e.method&&t.intent===e.intent),i=r?.find(t=>t.canHandleChallenge?.({challenge:e})??!0);if(i)return{method:i};if(r?.length)return{};let a=vP.find(t=>wP(t,e));return a?{plugin:a}:{}}function xP(e,t){let n=CP(t);if(n?.length){let r=jS(e,n,OS(n,t?.paymentPreferences).entries);if(r){let e=t?.plugins?.find(e=>wP(e,r.challenge));return{challenge:r.challenge,...e?{plugin:e}:{method:r.method}}}return}for(let n of e){let e=bP(n,t);if(e.plugin||e.method)return{challenge:n,...e}}}function SP(e){let t=CP(e);if(t?.length)return OS(t,e?.paymentPreferences).header}function CP(e){return Array.isArray(e?.methods)?e.methods.flat():void 0}function wP(e,t){return e.supports?e.supports(t):e.method===t.method}async function TP(e){let t=EP(e);if(!t)return;let n=await import(t);return{config:n.default??n,path:t}}function EP(e){if(e){let n=c.resolve(e);return t.existsSync(n)?n:void 0}let n=process.env.MPPX_CONFIG?.trim();if(n){let e=c.resolve(n);return t.existsSync(e)?e:void 0}}function DP(e,t){function n(t){let n={...t.request};if(t.method===`tempo`&&t.intent===`session`){let e={...n.methodDetails};delete e.sessionSnapshot,Object.keys(e).length?n.methodDetails=e:delete n.methodDetails}return Qx({...t,request:n,id:e.id,expires:e.expires})}if(n(t)!==n(e))throw Error(`Payment request changed on retry. Run the command again to approve the new payment.`)}pP(),gS(),Vj(),GM(),tN(),rO();function OP(e){return e??globalThis.fetch.bind(globalThis)}function kP(e,t){let n=e.request.methodDetails?.chainId;if(n!==void 0&&n!==t.chainId)throw Error(`Close challenge changed the session chain.`);if(typeof e.request.recipient!=`string`||e.request.recipient.toLowerCase()!==t.descriptor.payee.toLowerCase())throw Error(`Close challenge changed the session payee.`);if(typeof e.request.currency!=`string`||e.request.currency.toLowerCase()!==t.descriptor.token.toLowerCase())throw Error(`Close challenge changed the session token.`);if(Pj(e,t.escrow).toLowerCase()!==t.escrow.toLowerCase())throw Error(`Close challenge changed the session escrow.`);let r=wD(e);if(r&&r.channelId.toLowerCase()!==t.channelId.toLowerCase())throw Error(`Close challenge changed the session channel.`)}async function AP(e){kP(e.challenge,e.channel);let t=OP(e.fetch),n,r=async(r,i)=>{n&&=(await e.onChallenge?.(n),void 0);let a=await t(r,i);if(a.status!==402)return a;let o=sS(a).find(SD);if(!o)throw Error(`Refreshed close response did not include tempo/session.`);return kP(o,e.channel),n=o,a},i=eN({...e.manager,bootstrap:!1,escrow:e.channel.escrow,fetch:r});UM(i).rehydrate(e);let a=await i.close();if(!a)throw Error(`Session close response did not include a payment receipt.`);return{manager:i,receipt:a}}gS(),Vj(),qj(),rO(),cj(),Xx();var jP=class extends Error{name=`SessionStateError`;code=`SESSION_STATE_INVALID`;file;constructor(e,t={}){super(e,t.cause===void 0?void 0:{cause:t.cause}),this.file=t.file}},MP=class extends Error{name=`SessionBusyError`;code=`SESSION_BUSY`;exitCode=75;scope;owner;constructor(e,t){super(`Session scope is busy in process ${t.pid} on ${t.hostname}.`),this.scope=e,this.owner=t}};const NP=J.object({channelId:Kx(),cumulativeAmount:J.string(),deposit:J.string(),descriptor:J.object({payer:Gx(),payee:Gx(),operator:Gx(),token:Gx(),salt:Kx(),authorizedSigner:Gx(),expiringNonceHash:Kx()}),escrow:Gx(),chainId:J.number(),opened:J.boolean()}),PP=J.object({name:J.optional(J.string()),address:Gx()}),FP=J.object({method:J.literal(`tempo`),intent:J.literal(`session`),status:J.literal(`success`),timestamp:J.string(),reference:J.string(),challengeId:J.string(),channelId:Kx(),acceptedCumulative:J.string(),spent:J.string(),units:J.optional(J.number()),txHash:J.optional(Kx())}),IP=J.object({version:J.literal(1),method:J.literal(`tempo`),intent:J.literal(`session`),status:J.enum([`opening`,`open`,`closing`,`stale`]),channel:NP,account:PP,endpoint:J.string(),challenge:hS,receipt:J.optional(FP),spent:J.string(),units:J.number(),createdAt:J.string(),updatedAt:J.string()}),LP=J.object({version:J.literal(1),channelId:Kx()}),RP=J.object({version:J.literal(1),scope:J.string(),hostname:J.string(),pid:J.number(),token:J.string(),createdAt:J.string()});function zP(e,t,n,r){let i=e.safeParse(t);if(!i.success)throw Z(n,r,i.error);return i.data}function BP(e){let t=XP(e);return[t.payer,t.payee,t.token,t.escrow,t.chainId].join(`:`)}function VP(e){return{payer:e.descriptor.payer,payee:e.descriptor.payee,token:e.descriptor.token,escrow:e.escrow,chainId:e.chainId}}function HP(e={}){let t=e.stateRoot??WP(),n={root:t,channels:c.join(t,`channels`),locks:c.join(t,`locks`),preferred:c.join(t,`preferred`)},r=e.hostname??b.hostname(),i=e.pid??process.pid,a=e.now??(()=>new Date),o=e.isProcessAlive??SF;async function s(){for(let e of[n.root,n.channels,n.locks,n.preferred])await C.mkdir(e,{recursive:!0,mode:448}),await C.chmod(e,448)}async function l(e){let t=JP(e),r=GP(n,t),i=await pF(r);if(i===void 0)return;let a=iF(i,r);if(a.channel.channelId.toLowerCase()!==t)throw Z(r,`Session filename does not match its channel ID.`);return aF(a)}async function u(){let e;try{e=await C.readdir(n.channels,{withFileTypes:!0})}catch(e){if(wF(e,`ENOENT`))return[];throw Z(n.channels,`Unable to list managed sessions.`,e)}let t=[];for(let n of e){if(!n.isFile()||n.name.includes(`.tmp-`)||!n.name.endsWith(`.json`))continue;let e=n.name.slice(0,-5);JP(e);let r=await l(e);r&&t.push(r)}return t.sort((e,t)=>e.createdAt.localeCompare(t.createdAt))}async function d(e){await s();let t=eF(e.channel),r=JP(t.channelId),i=GP(n,r),o=await pF(i),c=o===void 0?void 0:iF(o,i);c&&oF(c,e,i);let l=nF(e.challenge,i);cF(l,t,i);let u=e.receipt?rF(e.receipt,t.channelId,i):void 0,d=c?Kj(c.channel):void 0,f=fF(t.cumulativeAmount,d?.cumulativeAmount??0n),p=fF(e.spent??0n,u?BigInt(u.spent):0n,c?BigInt(c.spent):0n);if(p>f)throw Z(i,`Session spend exceeds the locally authorized cumulative amount.`);let m=tF(Gj({...t,cumulativeAmount:f}),i),h=dF(c?.updatedAt,a(),i),g=uF(c?.receipt,u),_=$P(e.account,i),v=iF({version:1,method:`tempo`,intent:`session`,status:e.status,channel:m,account:{..._.name===void 0?c?.account.name===void 0?{}:{name:c.account.name}:{name:_.name},address:_.address},endpoint:QP(e.endpoint,i),challenge:l,...g!==void 0&&{receipt:g},spent:p.toString(),units:Math.max(c?.units??0,e.units??0,u?.units??0),createdAt:c?.createdAt??h,updatedAt:h},i);return await mF(i,v),aF(v)}async function f(e){let t=qP(n,e),r=await pF(t);if(r===void 0)return;let{channelId:i}=zP(LP,r,t,`Preferred session is invalid.`),a=await l(i);if(!a)throw Z(t,`Preferred session ${i} does not exist.`);return lF(a.channel,e,t),i}async function p(e,t){let r=JP(t),i=await l(r),a=qP(n,e);if(!i)throw Z(a,`Session ${r} does not exist.`);lF(i.channel,e,a),await mF(a,{version:1,channelId:r})}async function m(e,t){let r=qP(n,e),i=t===void 0?void 0:JP(t),a=await pF(r);if(a===void 0)return;let o=zP(LP,a,r,`Preferred session is invalid.`).channelId;i&&o.toLowerCase()!==i||await bF(r,`Unable to clear preferred session.`)}async function h(e){let t=JP(e),r=await l(t);r&&(await m(VP(r.channel),t),await bF(GP(n,t),`Unable to remove session.`))}async function g(e){await s();let t=KP(n,e);for(let n=0;n<3;n++){let n={version:1,scope:e,hostname:r,pid:i,token:_(),createdAt:a().toISOString()};try{if(await hF(t,n),await vF(_F(t))){await yF(t,n);continue}return{async release(){await yF(t,n)}}}catch(e){if(!wF(e,`EEXIST`))throw e}let s=await pF(t);if(s===void 0)continue;let c=zP(RP,s,t,`Session lock is invalid.`);if(c.scope!==e)throw Z(t,`Session lock scope is invalid.`);if(c.hostname!==r||o(c.pid))throw new MP(e,c);await gF(t,c)}let c=await pF(t);throw c===void 0?Z(t,`Unable to acquire session lock.`):new MP(e,zP(RP,c,t,`Session lock is invalid.`))}async function v(e){return g(BP(e))}return{root:t,get:l,list:u,upsert:d,remove:h,getPreferred:f,setPreferred:p,clearPreferred:m,acquire:v}}function UP(e,t){let n=ZP(t.scope),r=t.selection===`auto`||t.selection===`new`?void 0:JP(t.selection);function i(e){if(e.toLowerCase()!==n)throw new jP(`Session manager requested an unexpected payment scope.`)}async function a(n=!0){if(t.selection===`new`&&!r)return;let i=r??await e.getPreferred(t.scope);if(!i)return;let a=await e.get(i);if(!a)throw new jP(`Session ${i} does not exist.`);if(lF(a.channel,t.scope),!(n&&(a.status!==`open`||!a.channel.opened)))return r=i,a}return{async get(e){return i(e),(await a())?.channel},async set(n){if(i(Uj(n)),lF(n,t.scope),r&&r.toLowerCase()!==n.channelId.toLowerCase())throw new jP(`Session manager selected ${r}, but attempted to store ${n.channelId}.`);r||t.onNewChannel?.(n);let a=await e.upsert({...t.context(),channel:n});r=a.channel.channelId,await e.setPreferred(t.scope,a.channel.channelId)},async delete(n){i(n);let o=await a(!1);if(!o)return;let s=t.context();await e.upsert({...o,...s,status:s.status===`closing`?`closing`:`stale`,channel:o.channel,spent:s.spent??o.spent,units:s.units??o.units}),s.status!==`closing`&&await e.clearPreferred(t.scope,o.channel.channelId),r=void 0}}}function WP(){let e=process.env.XDG_STATE_HOME||c.join(b.homedir(),`.local`,`state`);return c.join(e,`mppx`,`sessions`,`v1`)}function GP(e,t){return c.join(e.channels,`${t}.json`)}function KP(e,t){let n=g(`sha256`).update(t).digest(`hex`);return c.join(e.locks,`${n}.lock`)}function qP(e,t){let n=g(`sha256`).update(BP(t)).digest(`hex`);return c.join(e.preferred,`${n}.json`)}function JP(e){return zP(Kx(),e,void 0,`Invalid session channel ID: ${e}.`).toLowerCase()}function YP(e,t,n){return zP(Gx(),e,n,`Invalid ${t}.`).toLowerCase()}function XP(e){if(!Number.isSafeInteger(e.chainId)||e.chainId<0)throw new jP(`Invalid session chain ID.`);return{payer:YP(e.payer,`session payer`),payee:YP(e.payee,`session payee`),token:YP(e.token,`session token`),escrow:YP(e.escrow,`session escrow`),chainId:e.chainId}}function ZP(e){let t=XP(e);return[t.payee,t.token,t.escrow,t.chainId].join(`:`)}function QP(e,t){if(typeof e!=`string`)throw Z(t,`Session endpoint is invalid.`);let n;try{n=new URL(e)}catch(e){throw Z(t,`Session endpoint is invalid.`,e)}if(n.protocol!==`http:`&&n.protocol!==`https:`)throw Z(t,`Session endpoint must use HTTP or HTTPS.`);if(n.username||n.password)throw Z(t,`Session endpoint must not contain credentials.`);let r=e.indexOf(`#`);return r===-1?e:e.slice(0,r)}function $P(e,t){return zP(PP,e,t,`Session account is invalid.`)}function eF(e){return Kj(tF(Gj(e)))}function tF(e,t){return zP(NP,e,t,`Stored session channel is invalid.`)}function nF(e,t){let n=hS.safeParse(e);if(!n.success||!SD(n.data))throw Z(t,`Stored session challenge is invalid.`);return n.data}function rF(e,t,n){let r=zP(FP,e,n,`Stored session receipt is invalid.`);if(r.channelId!==t.toLowerCase())throw Z(n,`Stored session receipt has a different channel ID.`);if(r.reference.toLowerCase()!==t.toLowerCase())throw Z(n,`Stored session receipt has a different reference.`);if(BigInt(r.spent)>BigInt(r.acceptedCumulative))throw Z(n,`Stored session receipt spend exceeds its accepted amount.`);return r}function iF(e,t){let n=zP(IP,e,t,`Stored session is invalid.`),{receipt:r,...i}=n,a=n.channel,o=n.account;if(o.address.toLowerCase()!==a.descriptor.payer.toLowerCase())throw Z(t,`Stored session account does not match the channel payer.`);let s=r?rF(r,a.channelId,t):void 0,c=n.spent,l=BigInt(a.cumulativeAmount);if(BigInt(c)>l)throw Z(t,`Stored session spend exceeds its cumulative authorization.`);if(s&&BigInt(s.acceptedCumulative)>l)throw Z(t,`Stored receipt exceeds the cumulative authorization.`);if(Date.parse(n.updatedAt)<Date.parse(n.createdAt))throw Z(t,`Stored session timestamps are not monotonic.`);let u=QP(n.endpoint,t);if(u!==n.endpoint)throw Z(t,`Stored session endpoint contains a fragment.`);let d=nF(n.challenge,t);return cF(d,Kj(a),t),{...i,account:o,channel:a,endpoint:u,challenge:d,...s&&{receipt:s}}}function aF(e){return{version:1,status:e.status,channel:Kj(e.channel),account:e.account,endpoint:e.endpoint,challenge:e.challenge,...e.receipt&&{receipt:e.receipt},spent:BigInt(e.spent),units:e.units,createdAt:e.createdAt,updatedAt:e.updatedAt}}function oF(e,t,n){let r=eF(t.channel),i=$P(t.account,n),a=Kj(e.channel);if(JSON.stringify(sF(r))!==JSON.stringify(sF(a)))throw Z(n,`Session update changed immutable channel identity.`);if(i.address.toLowerCase()!==e.account.address.toLowerCase())throw Z(n,`Session update changed the payer account.`)}function sF(e){return{channelId:e.channelId.toLowerCase(),descriptor:Object.fromEntries(Object.entries(e.descriptor).map(([e,t])=>[e,typeof t==`string`?t.toLowerCase():t])),escrow:e.escrow.toLowerCase(),chainId:e.chainId}}function cF(e,t,n){if(ij({...t.descriptor,chainId:t.chainId,escrow:t.escrow}).toLowerCase()!==t.channelId.toLowerCase())throw Z(n,`Session channel ID does not match its descriptor.`);let r=YP(e.request.recipient,`challenge recipient`,n),i=YP(e.request.currency,`challenge currency`,n);if(r!==t.descriptor.payee.toLowerCase())throw Z(n,`Session challenge payee does not match the channel.`);if(i!==t.descriptor.token.toLowerCase())throw Z(n,`Session challenge token does not match the channel.`);if(Pj(e,t.escrow).toLowerCase()!==t.escrow.toLowerCase())throw Z(n,`Session challenge escrow does not match the channel.`);if(CF(e.request.methodDetails)){let r=e.request.methodDetails;if(r.chainId!==void 0&&r.chainId!==t.chainId)throw Z(n,`Session challenge chain does not match the channel.`)}}function lF(e,t,n){let r=XP(t);if(e.descriptor.payer.toLowerCase()!==r.payer||e.descriptor.payee.toLowerCase()!==r.payee||e.descriptor.token.toLowerCase()!==r.token||e.escrow.toLowerCase()!==r.escrow||e.chainId!==r.chainId)throw Z(n,`Session channel does not match the selected payment scope.`)}function uF(e,t){return e?t&&Date.parse(t.timestamp)>=Date.parse(e.timestamp)?t:e:t}function dF(e,t,n){if(!Number.isFinite(t.getTime()))throw Z(n,`Session clock is invalid.`);let r=t.toISOString();return!e||Date.parse(r)>=Date.parse(e)?r:e}function fF(...e){return e.reduce((e,t)=>t>e?t:e,0n)}async function pF(e){let t;try{t=await C.readFile(e,`utf8`)}catch(t){if(wF(t,`ENOENT`))return;throw Z(e,`Unable to read session state.`,t)}try{return JSON.parse(t)}catch(t){throw Z(e,`Session state contains invalid JSON.`,t)}}async function mF(e,t){await C.mkdir(c.dirname(e),{recursive:!0,mode:448}),await C.chmod(c.dirname(e),448);let n=`${e}.tmp-${process.pid}-${_()}`,r;try{r=await C.open(n,`wx`,384),await r.writeFile(`${JSON.stringify(t,null,2)}\n`,`utf8`),await r.sync(),await r.close(),r=void 0,await C.rename(n,e),await C.chmod(e,384),await xF(c.dirname(e))}catch(t){throw await r?.close().catch(()=>void 0),await C.unlink(n).catch(()=>void 0),t instanceof jP?t:Z(e,`Unable to write session state.`,t)}}async function hF(e,t){let n;try{n=await C.open(e,`wx`,384),await n.writeFile(`${JSON.stringify(t)}\n`,`utf8`),await n.sync(),await n.close(),n=void 0,await xF(c.dirname(e))}catch(t){throw await n?.close().catch(()=>void 0),n&&await C.unlink(e).catch(()=>void 0),t}}async function gF(e,t){let n=_F(e);try{await C.link(e,n)}catch(t){if(wF(t,`EEXIST`)||wF(t,`ENOENT`))return;throw Z(e,`Unable to claim dead lock.`,t)}try{let r=await pF(n);if(r===void 0||zP(RP,r,n,`Session lock is invalid.`).token!==t.token)return;await bF(e,`Unable to reclaim dead lock.`)}finally{await bF(n,`Unable to release dead lock claim.`)}}function _F(e){return`${e}.reclaim`}async function vF(e){try{return await C.access(e),!0}catch(t){if(wF(t,`ENOENT`))return!1;throw Z(e,`Unable to inspect session lock.`,t)}}async function yF(e,t){let n=await pF(e);n!==void 0&&zP(RP,n,e,`Session lock is invalid.`).token===t.token&&await bF(e,`Unable to release session lock.`)}async function bF(e,t){try{await C.unlink(e),await xF(c.dirname(e))}catch(n){if(!wF(n,`ENOENT`))throw Z(e,t,n)}}async function xF(e){let t=await C.open(e,`r`);try{await t.sync()}finally{await t.close()}}function SF(e){try{return process.kill(e,0),!0}catch(e){if(wF(e,`ESRCH`))return!1;if(wF(e,`EPERM`))return!0;throw e}}function CF(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function wF(e,t){return e instanceof Error&&`code`in e&&e.code===t}function Z(e,t,n){return new jP(t,{...n!==void 0&&{cause:n},file:e})}Dx(),oT(),FM(),Qj(),cj(),ZO(),LO();const TF=p.object({status:p.enum([`opening`,`open`,`closing`,`stale`]),channelId:p.string(),account:p.string().optional(),payer:p.string(),payee:p.string(),authorizedSigner:p.string(),token:p.string(),escrow:p.string(),chainId:p.number(),cumulativeAmount:p.string(),confirmedSpend:p.string(),deposit:p.string(),units:p.number(),resourceUrl:p.string(),createdAt:p.string(),updatedAt:p.string()}),EF=p.object({channelId:p.string(),status:p.enum([`closed`,`already-closed`]),spent:p.string(),txHash:p.string().optional()}),DF=p.object({closed:p.array(EF),failed:p.array(p.object({channelId:p.string(),message:p.string()}))});function OF(e){return e===`mainnet`?ve.id:ye.id}function kF(e){if(e===ve.id)return`mainnet`;if(e===ye.id)return`testnet`}function AF(e){let t={};for(let n of e??[]){let e=n.indexOf(`:`);if(e===-1)throw new I({code:`INVALID_HEADER`,message:`Invalid header format: ${n}`,exitCode:2});t[n.slice(0,e).trim()]=n.slice(e+1).trim()}return t}function jF(e){return{status:e.status,channelId:e.channel.channelId,...e.account.name&&{account:e.account.name},payer:e.channel.descriptor.payer,payee:e.channel.descriptor.payee,authorizedSigner:e.channel.descriptor.authorizedSigner,token:e.channel.descriptor.token,escrow:e.channel.escrow,chainId:e.channel.chainId,cumulativeAmount:e.channel.cumulativeAmount.toString(),confirmedSpend:e.spent.toString(),deposit:e.channel.deposit.toString(),units:e.units,resourceUrl:e.endpoint,createdAt:e.createdAt,updatedAt:e.updatedAt}}function MF(e,t){throw e instanceof I?e:e instanceof MP?new I({code:e.code,message:e.message,exitCode:e.exitCode,cause:e}):e instanceof jP?new I({code:e.code,message:e.message,exitCode:65,cause:e}):new I({code:t,message:e instanceof Error?e.message:String(e),exitCode:75,...e instanceof Error&&{cause:e}})}async function NF(e,t){let n=await qO(t??e.account.name);if(!yM(n.account,e.channel.descriptor))throw new I({code:`SESSION_ACCOUNT_MISMATCH`,message:`Account ${n.account.address} cannot sign for session ${e.channel.channelId}.`,exitCode:2});return n}async function PF(e,t){if(t.network&&OF(t.network)!==e)throw new I({code:`CHAIN_MISMATCH`,message:`Session uses chainId ${e}, not ${t.network}.`,exitCode:2});let n=t.network??kF(e),r=CO(t.rpcUrl,{network:n}),i=await TO({network:n,rpcUrl:r});if(i.id!==e)throw new I({code:`CHAIN_MISMATCH`,message:`Session uses chainId ${e}, but RPC is chainId ${i.id}.`,exitCode:2});return j({chain:i,transport:ue(r)})}const FF=sb(`sessions`,{description:`Manage persistent payment sessions (list, view, close)`}).command(`list`,{description:`List persistent payment sessions`,options:p.object({account:p.string().optional().describe(`Filter by account name`),network:p.enum([`mainnet`,`testnet`]).optional().describe(`Filter by Tempo network`)}),output:p.object({sessions:p.array(TF)}),alias:{account:`a`},async run(e){try{let t=(await HP().list()).filter(t=>!(e.options.account&&t.account.name!==e.options.account||e.options.network&&t.channel.chainId!==OF(e.options.network))).map(jF),n={sessions:t};if(e.format===`json`&&e.formatExplicit)return e.ok(n);t.length===0&&console.log(`No sessions.`);for(let[e,n]of t.entries())e>0&&console.log(``),IF(n);return}catch(e){return MF(e,`SESSION_LIST_FAILED`)}}}).command(`view`,{description:`View a persistent payment session`,args:p.object({channelId:p.string().describe(`Full session channel ID`)}),output:TF,async run(e){try{let t=await HP().get(e.args.channelId);if(!t)throw new I({code:`SESSION_NOT_FOUND`,message:`Session ${e.args.channelId} was not found.`,exitCode:2});let n=jF(t);if(e.format===`json`&&e.formatExplicit)return e.ok(n);IF(n);return}catch(e){return MF(e,`SESSION_VIEW_FAILED`)}}}).command(`close`,{description:`Cooperatively close persistent payment sessions`,usage:[{suffix:`<channel-id> [options]`},{suffix:`--all --yes [options]`}],args:p.object({channelId:p.string().optional().describe(`Full session channel ID`)}),options:p.object({account:p.string().optional().describe(`Account name`),all:p.boolean().optional().default(!1).describe(`Close every matching session`),header:p.array(p.string()).optional().describe(`Add close request header (repeatable)`),network:p.enum([`mainnet`,`testnet`]).optional().describe(`Tempo network`),rpcUrl:p.string().optional().describe(`RPC endpoint (env: MPPX_RPC_URL)`),url:p.string().optional().describe(`Override the stored resource URL`),yes:p.boolean().optional().default(!1).describe(`Confirm closing every session`)}),output:p.union([EF,DF]),alias:{account:`a`,header:`H`,rpcUrl:`r`},async run(e){if(e.options.all&&e.args.channelId)return e.error({code:`INVALID_SESSION_CLOSE`,message:`Specify a channel ID or --all, not both.`,exitCode:2});if(!e.options.all&&!e.args.channelId)return e.error({code:`INVALID_SESSION_CLOSE`,message:`Specify a channel ID or --all.`,exitCode:2});if(e.options.all&&!e.options.yes)return e.error({code:`CONFIRMATION_REQUIRED`,message:`Closing all sessions requires --yes.`,exitCode:2});if(e.options.all&&(e.options.header||e.options.rpcUrl||e.options.url))return e.error({code:`INVALID_SESSION_CLOSE`,message:`--header, --rpc-url, and --url only apply to a single session.`,exitCode:2});let t=HP();async function n(e,n={}){let r=await t.get(e);if(!r)throw new I({code:`SESSION_NOT_FOUND`,message:`Session ${e} was not found.`,exitCode:2});let i=VP(r.channel),a=await t.acquire(i);try{let r=await t.get(e);if(!r)throw new I({code:`SESSION_NOT_FOUND`,message:`Session ${e} was not found.`,exitCode:2});let a=await NF(r,n.account),o=await PF(r.channel.chainId,n);if(ij({...r.channel.descriptor,escrow:r.channel.escrow,chainId:r.channel.chainId}).toLowerCase()!==r.channel.channelId.toLowerCase())throw new I({code:`SESSION_STATE_INVALID`,message:`Stored descriptor does not derive the session channel ID.`,exitCode:65});if((await Yj(o,r.channel.channelId,r.channel.escrow)).deposit===0n)return await t.remove(r.channel.channelId),{channelId:r.channel.channelId,status:`already-closed`,spent:r.spent.toString()};let s=QP(n.resourceUrl??r.endpoint),c={...a.source===`keychain`&&{name:a.accountName},address:a.account.address},l=(e=r.challenge)=>({status:`closing`,account:c,endpoint:s,challenge:e,...r.receipt&&{receipt:r.receipt},spent:r.spent,units:r.units}),u=await t.upsert({...l(),channel:r.channel}),d=AF(n.headers),f=u.challenge,p=await AP({channel:u.channel,challenge:u.challenge,fetch:async(e,t)=>globalThis.fetch(e,{...t,headers:{...d,...Sw(t?.headers)}}),input:s,spent:u.spent,async onChallenge(e){f=e,await t.upsert({...l(e),channel:u.channel})},manager:{account:a.account,client:o,channelStore:UP(t,{scope:i,selection:u.channel.channelId,context:()=>l(f)})}});return await t.remove(u.channel.channelId),{channelId:u.channel.channelId,status:`closed`,spent:p.receipt.spent,txHash:p.receipt.txHash}}finally{await a.release()}}try{if(e.options.all){let r=(await t.list()).filter(t=>(!e.options.account||t.account.name===e.options.account)&&(!e.options.network||t.channel.chainId===OF(e.options.network))),i=[],a=[];for(let t of r)try{i.push(await n(t.channel.channelId,{account:e.options.account,network:e.options.network}))}catch(e){a.push({channelId:t.channel.channelId,message:e instanceof Error?e.message:String(e)})}let o={closed:i,failed:a};if(a.length>0){for(let e of a)process.stderr.write(`${e.channelId}: ${e.message}\n`);process.exitCode=1}if(e.format===`json`&&e.formatExplicit)return e.ok(o);for(let e of i)console.log(`${e.channelId} ${e.status} ${e.spent}`);for(let e of a)console.log(`${e.channelId} failed ${e.message}`);return}let r=await n(e.args.channelId,{account:e.options.account,headers:e.options.header,network:e.options.network,resourceUrl:e.options.url,rpcUrl:e.options.rpcUrl});if(e.format===`json`&&e.formatExplicit)return e.ok(r);console.log(`${r.channelId} ${r.status} ${r.spent}`),r.txHash&&console.log(` transaction ${r.txHash}`);return}catch(e){return MF(e,`SESSION_CLOSE_FAILED`)}}});function IF(e){console.log(e.channelId),console.log(` status ${e.status}`),console.log(` cumulative amount ${e.cumulativeAmount}`),console.log(` confirmed spend ${e.confirmedSpend}`),console.log(` deposit ${e.deposit}`),console.log(` chain ${e.chainId}`),console.log(` payer ${e.payer}`),console.log(` payee ${e.payee}`),console.log(` token ${e.token}`),console.log(` resource ${e.resourceUrl}`)}Dx(),FM(),GM(),tN(),ME(),ZO(),LO();function LF(e,t){if(t&&e!==`auto`&&t.toLowerCase()!==e.toLowerCase())throw new I({code:`SESSION_SELECTION_CONFLICT`,message:`--session and -M channel= select different sessions.`,exitCode:2});let n=t??e;if(n===`auto`||n===`new`)return n;if(/^0x[0-9a-fA-F]{64}$/.test(n))return n.toLowerCase();throw new I({code:`INVALID_SESSION`,message:`Session must be auto, new, or a 32-byte channel ID.`,exitCode:2})}function RF(e){let t=e.allowCustomEscrow;if(t!==void 0){if(t===`true`)return!0;if(t===`false`)return!1;throw new I({code:`INVALID_ALLOW_CUSTOM_ESCROW`,message:`allowCustomEscrow must be true or false.`,exitCode:2})}}function zF(e,t,n,r){if(t.deposit!==void 0)return t.deposit;let i=e.request.suggestedDeposit;return typeof i==`string`?ce(BigInt(i),r):n?`10`:void 0}function BF(e,t){if(!e)return;if(e.challengeId!==t.challenge.id)throw Error(`Session receipt challenge does not match the paid request.`);if(e.channelId.toLowerCase()!==t.channelId.toLowerCase())throw Error(`Session receipt channel does not match the selected channel.`);let n=BigInt(e.acceptedCumulative),r=BigInt(e.spent);if(n>t.cumulative)throw Error(`Session receipt exceeds the local cumulative authorization.`);if(r>n)throw Error(`Session receipt spend exceeds its accepted authorization.`);return e}function VF(e,t){return e.channel.channelId.toLowerCase()===t.toLowerCase()}function HF(e){if(e.trim()!==`[DONE]`){if(e.length===0){process.stdout.write(`
|
|
243
243
|
`);return}try{let t=JSON.parse(e);process.stdout.write(t.token??t.choices?.[0]?.delta?.content??e)}catch{process.stdout.write(e)}}}async function UF(e){let{options:t}=e,n=await qO(t.account),r=CO(t.rpcUrl,{network:t.network}),i=await TO({network:t.network,rpcUrl:r}),a=j({chain:i,transport:ue(r)}),o=RF(e.methodOptions),s=await pM({allowCustomEscrow:o,challenge:e.challenge,getClient:async()=>a});if(s.chainId!==i.id)throw new I({code:`CHAIN_MISMATCH`,message:`Challenge requires chainId ${s.chainId}, but RPC is chainId ${i.id}.`,exitCode:2});let{decimals:c}=await kO(a,s.token,n.account.address,{requireDecimals:!0}),l={payer:n.account.address,payee:s.payee,token:s.token,escrow:s.escrow,chainId:s.chainId},u=LF(t.session,e.methodOptions.channel),d=e.registry??HP(),f=await d.acquire(l).catch(e=>{throw e instanceof MP?new I({code:e.code,message:e.message,exitCode:e.exitCode,cause:e}):e});try{let r=u===`auto`?await d.getPreferred(l):u===`new`?void 0:u,s=r?await d.get(r):void 0;if(r&&!s)throw new I({code:`SESSION_NOT_FOUND`,message:`Session ${r} was not found.`,exitCode:2});if(s&&u!==`auto`&&(s.status!==`open`||!s.channel.opened||BP(VP(s.channel))!==BP(l)||!yM(n.account,s.channel.descriptor)))throw new I({code:`SESSION_MISMATCH`,message:`Session ${s.channel.channelId} cannot be used for this account and challenge.`,exitCode:2});let f=s?.status===`open`&&s.channel.opened?s:void 0,p=f?`open`:`opening`,m=e.challenge,h=f?.receipt,g=f?.spent??0n,_=f?.units??0,v=QP(e.endpoint),y={...n.source===`keychain`&&{name:n.accountName},address:n.account.address},b=UP(d,{scope:l,selection:u,context:()=>({status:p,account:y,endpoint:v,challenge:m,...h&&{receipt:h},spent:g,units:_}),onNewChannel(){p=`opening`,h=void 0,g=0n,_=0}}),{signal:x,...S}=e.init,C={...S,...x&&{signal:x},onReceipt(e){h=e,g=g>BigInt(e.spent)?g:BigInt(e.spent),_=Math.max(_,e.units??0)}};if(e.challengeResponse.status!==402||e.challengeResponse.bodyUsed)throw Error(`Session manager requires an unconsumed 402 challenge response.`);let w=!0,T=eN({account:n.account,allowCustomEscrow:o,bootstrap:!1,client:a,channelStore:b,credentialContext:e.credentialContext,decimals:c,maxDeposit:zF(e.challenge,e.methodOptions,OO(i),c),fetch:async(t,n)=>w?(w=!1,e.challengeResponse):e.fetch(t,n)});f&&UM(T).rehydrate({channel:f.channel,challenge:e.challenge,input:e.fetchInput,spent:f.spent});let{onReceipt:ee,...E}=C,D=await T.fetch(e.fetchInput,E);if(t.fail&&D.status>=400)throw new I({code:`HTTP_ERROR`,message:`HTTP error ${D.status}`,exitCode:22});if(D.status===402)throw new I({code:`PAYMENT_REJECTED`,message:`Payment rejected.`,exitCode:75});fO(D,{include:!1,verbose:t.include?Math.max(t.verbose,2):t.verbose,silent:t.silent}),m=D.challenge??e.challenge;let O=T.channelId;if(!O)throw Error(`Session manager did not select a channel.`);if(h=BF(D.receipt,{challenge:m,channelId:O,cumulative:T.cumulative})??h,SE(D)){let t=UM(T).consumeSseResponse(e.fetchInput,D,{onReceipt:ee,signal:e.init.signal??void 0});for await(let e of t)HF(e)}else process.stdout.write(Buffer.from(await D.arrayBuffer()));let k=await d.get(O);if(!k||!VF(k,O))throw Error(`Session ${O} was not persisted.`);if(h){let e=BF(h,{challenge:m,channelId:O,cumulative:T.cumulative});if(!e)throw Error(`Session receipt validation failed.`);h=e,g=g>BigInt(e.spent)?g:BigInt(e.spent),_=Math.max(_,e.units??0),p=`open`}await d.upsert({status:p,channel:k.channel,account:y,endpoint:v,challenge:m,...h&&{receipt:h},spent:g,units:_}),t.silent||(e.info(`Session retained ${O}\n`),e.info(`Close with: mppx sessions close ${O}\n`))}finally{await f.release()}}LO();function Q(e,t){return t?{label:e,detail:t,severity:`pass`}:{label:e,severity:`pass`}}function $(e,t,n){let r={label:e,severity:`fail`};return t&&(r.detail=t),n&&(r.hint=n),r}function WF(e,t,n){let r={label:e,severity:`warn`};return t&&(r.detail=t),n&&(r.hint=n),r}function GF(e,t,n){let r=t?{label:e,detail:t,severity:`skip`}:{label:e,severity:`skip`};return n&&(r.hint=n),r}const KF={pass:X.green(`✓`),fail:X.red(`✗`),warn:X.yellow(`⚠`),suggested:X.cyan(`◇`),skip:X.dim(`○`)};function qF(e){let t=KF[e.severity],n=e.detail?`${e.label} (${e.detail})`:e.label;console.log(` ${t} ${n}`),e.hint&&e.severity!==`pass`&&console.log(X.dim(` → ${e.hint}`))}function JF(e){console.log(`\n${X.bold(e)}`)}function YF(e,t){for(let n of e)qF(n),n.severity===`pass`?t.passed++:n.severity===`fail`?t.failed++:n.severity===`warn`?t.warnings++:n.severity===`suggested`?t.suggested++:n.severity===`skip`&&t.skipped++}async function XF(e,t,n=15e3){let r=new AbortController,i=setTimeout(()=>r.abort(),n);try{return await fetch(e,{...t,signal:r.signal})}finally{clearTimeout(i)}}function ZF(e){return e<1024?`${e}B`:e<1048576?`${(e/1024).toFixed(1)}KB`:`${(e/1048576).toFixed(1)}MB`}const QF=new Set([`get`,`post`,`put`,`patch`,`delete`,`head`,`options`,`trace`]);function $F(e){return typeof e==`string`&&/^0x[0-9a-fA-F]{40}$/.test(e)}function eI(e){return typeof e==`string`&&/^(0|[1-9][0-9]*)$/.test(e)}function tI(e){let t=e.indexOf(`:`);if(t<1)return null;let n=e.slice(0,t).toLowerCase();if(!QF.has(n))return null;let r=e.slice(t+1);if(!r)return null;let i=r.startsWith(`/`)?r:`/`+r;return{method:n.toUpperCase(),path:i}}function nI(e,t){if(e){try{let n=JSON.parse(e);if(n&&typeof n==`object`&&!Array.isArray(n)){let e=Object.keys(n);if(e.length>0&&e.every(e=>e.startsWith(`/`))){let e=n[t];return e===void 0?void 0:typeof e==`string`?e:JSON.stringify(e)}}}catch{}return e}}function rI(e){if(!e)return{};let t={};for(let n of e){let e=n.indexOf(`:`);e>0&&(t[n.slice(0,e).trim().toLowerCase()]=n.slice(e+1).trim())}return t}async function iI(e){let t=new URL(`openapi.json`,e.replace(/\/?$/,`/`)).href;try{let e=await XF(t,{});if(!e.ok)return{error:`HTTP ${e.status}`};let n=await e.text();return{doc:JSON.parse(n),raw:n}}catch(e){return e instanceof DOMException&&e.name===`AbortError`?{error:`Request timed out after 15s`}:e instanceof SyntaxError?{error:`Invalid JSON`}:{error:e.message}}}async function aI(e){let t=new URL(`/llms.txt`,e).href,n=`llms.txt found and nonempty`;try{let e=await XF(t,{});if(!e.ok)throw Error(`HTTP ${e.status}`);let r=(await e.text()).trim();if(!r)throw Error(`Empty document`);let i=e.headers.get(`content-type`)?.split(`;`)[0]?.trim().toLowerCase();if(i&&!i.startsWith(`text/`)||i===`text/html`||/^<(?:!doctype html|html|head|body)\b/i.test(r))throw Error(`Expected text documentation, received non-text or HTML content`);return Q(n,t)}catch(e){return{label:n,detail:`${t}: ${e.message}`,severity:`suggested`,hint:`Serve nonempty text documentation at /llms.txt.`}}}function oI(e){let t=[],n=[],r=e.paths;if(!r)return[];for(let[e,i]of Object.entries(r)){let r=lI(i.parameters);for(let[a,o]of Object.entries(i)){if(!QF.has(a)||!o||typeof o!=`object`||Array.isArray(o))continue;let i=o,s=uI(r,lI(i.parameters)),c=s.length>0?s:void 0;if(i[`x-payment-info`]){let n=i[`x-payment-info`];t.push({method:a.toUpperCase(),path:e,amount:n.amount,parameters:c})}else{let t=i.responses;t&&`402`in t&&n.push({method:a.toUpperCase(),path:e,parameters:c})}}}return t.length>0?t:n}function sI(e,t){let n=e.paths;if(!n)return;let r=n[t.path];if(!r)return;let i=r[t.method.toLowerCase()];if(!i?.requestBody)return;let a=i.requestBody.content?.[`application/json`];if(!a)return;if(a.example)return JSON.stringify(a.example);if(a.examples&&typeof a.examples==`object`){let e=Object.values(a.examples)[0];if(e?.value)return JSON.stringify(e.value)}let o=a.schema,s=new Set;if(o=fI(o,e,s),!o||o.type!==`object`)return;let c=pI(o,e,s);if(c&&typeof c==`object`&&Object.keys(c).length>0)return JSON.stringify(c)}function cI(e,t,n){if(!t.path.startsWith(`/`)||t.path.startsWith(`//`)||t.path.includes(`\\`))throw Error(`OpenAPI path must be an origin-relative path: ${t.path}`);let r=t.path;t.parameters&&(r=dI(r,t.parameters));let i=r.startsWith(`/`)?r.slice(1):r,a=new URL(e.replace(/\/?$/,`/`)),o=new URL(i,a);if(o.origin!==a.origin)throw Error(`OpenAPI path must be an origin-relative path: ${t.path}`);let s=o.href;if(n){let e=new URL(s);for(let t of n){let[n,...r]=t.split(`=`);n&&e.searchParams.set(n,r.join(`=`))}s=e.href}return s}function lI(e){if(!Array.isArray(e))return[];let t=[];for(let n of e){if(!n||typeof n!=`object`)continue;let e=n;e.in===`path`&&typeof e.name==`string`&&t.push(e.schema&&typeof e.schema==`object`?{name:e.name,in:`path`,schema:e.schema,example:e.example}:{name:e.name,in:`path`,example:e.example})}return t}function uI(e,t){let n=[...e];for(let e of t){let t=n.findIndex(t=>t.name===e.name);t>=0?n[t]=e:n.push(e)}return n}function dI(e,t){return e.replace(/\{([^}]+)\}/g,(e,n)=>{let r=t.find(e=>e.name===n&&e.in===`path`);if(!r)return e;let i=r.example??r.schema?.example??r.schema?.default;return i===void 0?e:encodeURIComponent(String(i))})}function fI(e,t,n){if(!e||typeof e.$ref!=`string`)return e;if(n?.has(e.$ref))return;let r=e.$ref.replace(/^#\//,``).split(`/`),i=t;for(let e of r)if(i&&typeof i==`object`)i=i[e];else return;return i}function pI(e,t,n){let r=fI(e,t,n)??e;if(r.const!==void 0)return r.const;if(r.example!==void 0)return r.example;if(r.default!==void 0)return r.default;switch(r.type){case`string`:return r.enum&&Array.isArray(r.enum)?r.enum[0]:r.format===`email`?`test@example.com`:r.format===`uuid`?`00000000-0000-0000-0000-000000000000`:r.format===`uri`||r.format===`url`?`https://example.com`:r.format===`date`?`2026-01-01`:r.pattern===`^\\d{5}(?:-\\d{4})?$`?`10001`:r.pattern===`^[A-Z]{2}$`?`US`:`test`;case`number`:case`integer`:return r.minimum??1;case`boolean`:return!0;case`array`:return[];case`object`:{let e=r.properties;if(!e)return{};let i=r.required||[],a={};for(let r of i){let i=e[r];i&&(a[r]=pI(i,t,n))}return a}default:return null}}gS(),Px(),ew(),LO();const mI=Object.values(Be);function hI(e){let t=e.request.methodDetails;return typeof t?.chainId==`number`&&mI.find(e=>e.id===t.chainId)?.testnet===!0}async function gI(e,t,n,r){let i=[],a;try{a=cI(e,t,r?.query)}catch(e){return i.push($(`Request URL valid`,e.message)),{results:i}}let o=rI(r?.extraHeaders),s;r?.body&&!r.bodyIsAutogenerated&&(s=r.body,o[`content-type`]||=`application/json`);let c;try{let e={method:t.method};Object.keys(o).length>0&&(e.headers=o),s&&(e.body=s),c=await XF(a,e)}catch(e){return i.push($(`Request failed`,e.message)),{results:i}}if(!s&&(c.status===400||c.status===422)){let e=r?.body??(r?.discoveryDoc?sI(r.discoveryDoc,t):void 0);if(e){n&&console.log(X.dim(` Retrying with body: ${e}`)),s=e,o[`content-type`]=`application/json`;try{c=await XF(a,{method:t.method,headers:o,body:s})}catch(e){return i.push($(`Request failed`,e.message)),{results:i}}}}if(c.status!==402)return c.status===200?i.push(GF(`Returns 402 without credentials`,`Got 200 (endpoint may not require payment in all cases)`)):c.status===401||c.status===403?i.push(GF(`Returns 402 without credentials`,`Got ${c.status} (endpoint requires auth before payment gate)`)):i.push($(`Returns 402 without credentials`,`Got ${c.status} instead`,c.status===400||c.status===422?`Server requires a valid request body before returning 402. Add a requestBody schema with examples to your OpenAPI doc, or use --body.`:`Endpoints that require payment must return HTTP 402 with a WWW-Authenticate: Payment header when no valid credential is provided.`)),{results:i};i.push(Q(`Returns 402 without credentials`));let l=c.headers.get(q.wwwAuthenticate),u=DI(c);if(!l)return u.length>0?(i.push(GF(`Not an MPP endpoint`,`No WWW-Authenticate header — x402 protocol detected`)),i.push(...u)):i.push(GF(`Not an MPP endpoint`,`No WWW-Authenticate header (may be x402 or other protocol)`)),{results:i};if(u.length>0&&i.push(...u),!l.startsWith(`${kx.payment} `))return i.push(GF(`Not an MPP endpoint`,`WWW-Authenticate scheme is not Payment`)),{results:i};i.push(Q(`WWW-Authenticate header present`,`Payment scheme`));let d;try{d=sS(c)}catch(e){let t=e.message;i.push($(`Challenge parseable`,t,`The Payment challenge must include a request="<base64url>" parameter containing JSON-encoded payment details.`));let n=l.slice(l.indexOf(` `)+1);return i.push(WF(`Received`,n.length>200?n.slice(0,200)+`...`:n)),{results:i}}if(d.length===0)return i.push($(`Challenge parseable`,`No Payment challenges found in header`)),{results:i};let f=d.map(e=>`${e.method}/${e.intent}`).join(`, `);i.push(Q(`Challenge parseable`,d.length===1?f:`${d.length} methods: ${f}`));let p=new URL(e).hostname;_I(d,i),vI(d,i),yI(d,i),bI(d,p,i);let m=d.length>1;for(let e of d){let t=e.request;xI(e,t,i,m)}if(n)for(let e of d)console.log(X.dim(` Challenge (${e.method}/${e.intent}): ${JSON.stringify(e,null,2)}`));return{results:i,resolvedBody:s,challenges:d}}function _I(e,t){let n=e.filter(e=>!e.id);n.length===0?t.push(Q(`Challenge has id`)):t.push($(`Challenge has id`,`${n.length} missing`,`Every challenge must include a unique id field.`))}function vI(e,t){let n=e.filter(e=>!e.realm);n.length===0?t.push(Q(`Challenge has realm`)):t.push($(`Challenge has realm`,`${n.length} missing`,`Set realm to your server's hostname.`))}function yI(e,t){let n=new Date;for(let r of e){if(!r.expires){t.push(WF(`Challenge has expiration`,`missing expires field`,`Add an expires field (ISO 8601) to prevent replay attacks.`));return}if(new Date(r.expires)<=n){t.push($(`Challenge expires in the future`,`Expired at ${r.expires}`,`The expires timestamp must be in the future.`));return}}let r=Math.min(...e.map(e=>new Date(e.expires).getTime()-n.getTime()));t.push(Q(`Challenge expires in the future`,`${Math.round(r/6e4)}m from now`))}function bI(e,t,n){let r=e=>e.toLowerCase().replace(/\.$/,``),i=r(t),a=[...new Set(e.map(e=>e.realm??``))].filter(e=>{let t=r(e);return!t||t!==i&&!i.endsWith(`.${t}`)});a.length>0?n.push($(`Realm matches server hostname`,`realm="${a[0]}" vs host="${t}"`,`Set the realm to your production hostname (or base domain) in the challenge.`)):n.push(Q(`Realm matches server hostname`))}function xI(e,t,n,r){let i=r?`[${e.method}] `:``;e.method===Ax.tempo?SI(t,i,n,e):e.method===Ax.stripe?CI(t,i,n):e.method===Ax.evm&&wI(e,t,i,n)}function SI(e,t,n,r){let i=e.methodDetails,a=Array.isArray(i?.splits)&&i.splits.length>0;if($F(e.recipient)?n.push(Q(`${t}Valid recipient address`)):e.recipient===void 0&&a?n.push(Q(`${t}Uses splits (no single recipient)`)):e.recipient===void 0?n.push($(`${t}Valid recipient address`,`Missing recipient (and no splits defined)`,`Set request.recipient to a valid 0x address, or use methodDetails.splits for multiple recipients.`)):n.push($(`${t}Valid recipient address`,`Got: ${String(e.recipient)}`,`Set request.recipient to a valid 0x-prefixed 40-hex-char address.`)),$F(e.currency)){let e=hI(r)?`testnet`:`mainnet`;n.push(Q(`${t}Valid currency address`,e))}else n.push($(`${t}Valid currency address`,`Got: ${String(e.currency)}`,`Set request.currency to a valid token address.`));TI(e,t,n,`token's smallest unit (e.g. "10000" = $0.01 for 6-decimal tokens)`)}function CI(e,t,n){TI(e,t,n,`currency's smallest unit (e.g. "100" = $1.00 for USD)`);let r=e.currency;typeof r==`string`&&/^[a-z]{3}$/i.test(r)?n.push(Q(`${t}Valid currency code`,r.toUpperCase())):n.push($(`${t}Valid currency code`,r?`Got: ${r}`:`missing`,`Must be a three-letter ISO currency code (e.g. "usd").`));let i=e.methodDetails,a=i?.networkId;a?n.push(Q(`${t}Has networkId`,a.slice(0,20))):n.push($(`${t}Has networkId`,`Missing methodDetails.networkId`,`Set methodDetails.networkId to your Stripe Business Network profile ID.`));let o=i?.paymentMethodTypes;Array.isArray(o)&&o.length>0?n.push(Q(`${t}Has paymentMethodTypes`,o.join(`, `))):n.push($(`${t}Has paymentMethodTypes`,`Missing or empty`,`Set methodDetails.paymentMethodTypes to an array (e.g. ["card"]).`))}function wI(e,t,n,r){if($F(t.recipient)?r.push(Q(`${n}Valid recipient address`)):r.push($(`${n}Valid recipient address`,`Got: ${String(t.recipient)}`,`Set request.recipient to a valid 0x-prefixed Ethereum address.`)),$F(t.currency)){let t=hI(e)?`testnet`:`mainnet`;r.push(Q(`${n}Valid currency address`,t))}else r.push($(`${n}Valid currency address`,`Got: ${String(t.currency)}`,`Set request.currency to a valid ERC-20 token contract address.`));TI(t,n,r,`token's smallest unit`);let i=t.methodDetails?.chainId;typeof i==`number`&&i>0?r.push(Q(`${n}Has chainId`,String(i))):r.push($(`${n}Has chainId`,`Missing methodDetails.chainId`,`Set methodDetails.chainId to the EVM chain ID.`))}function TI(e,t,n,r){eI(e.amount)?n.push(Q(`${t}Amount is valid integer string`)):e.amount===void 0||e.amount===null?n.push($(`${t}Amount is valid integer string`,`Missing amount`,`Set request.amount to a string of digits in the ${r}.`)):n.push($(`${t}Amount is valid integer string`,`Got: ${String(e.amount)}`,`request.amount must be a string of digits (no decimals, no prefix).`))}async function EI(e,t,n){let r=[],i=cI(e,t,n?.query),a={[q.authorization]:`${kx.payment} dGhpcyBpcyBnYXJiYWdl`},o;n?.body&&(o=n.body,a[`content-type`]=`application/json`);try{let e=await XF(i,{method:t.method,headers:a,body:o??null});e.status===402?(r.push(Q(`Malformed credential returns 402`,`not 500`)),e.headers.get(q.wwwAuthenticate)?.startsWith(`${kx.payment} `)?r.push(Q(`Error response includes fresh challenge`)):r.push(WF(`Error response includes fresh challenge`,`No WWW-Authenticate header`,`When rejecting an invalid credential, respond with 402 and include a fresh WWW-Authenticate: Payment challenge so the client can retry.`))):e.status>=500?r.push($(`Malformed credential returns 402`,`Got ${e.status} (server error)`,`When the Authorization header contains an invalid credential, respond with 402 (not 500). Catch credential validation errors and return a fresh challenge.`)):r.push(WF(`Malformed credential returns 402`,`Got ${e.status}`,`When the Authorization header contains an invalid Payment credential, respond with 402 and a fresh WWW-Authenticate challenge. Returning ${e.status} prevents the client from retrying with a valid payment.`))}catch(e){r.push($(`Error handling test`,e.message))}return r}function DI(e){let t=[],n=e.headers.get(`payment-required`)??e.headers.get(`x-payment-required`);if(!n)return t;let r=e.headers.get(`payment-required`)?`PAYMENT-REQUIRED`:`X-Payment-Required`;try{let e=qC(n);t.push(Q(`x402 payment challenge`,`${r} with valid x402 v${e.x402Version} payload`))}catch{t.push(WF(`x402 payment challenge`,`${r} header present but failed schema validation`,`Ensure the PAYMENT-REQUIRED header contains a valid base64-encoded JSON payload matching the x402 schema.`))}return t}const OI=[`No discovery document found, endpoints could not be automatically found.`,`MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.`,`The MPP SDKs can automatically expose this endpoint. See https://mpp.dev/sdk.`,``,`To test a specific endpoint: mppx validate <url> --endpoint POST:/your/path`].join(`
|
|
244
244
|
`);Xx();const kI={fundingCurrency:J.optional(J.string()),method:J.string(),reference:J.string(),externalId:J.optional(J.string()),subscriptionId:J.optional(J.string()),status:J.literal(`success`),timestamp:Vx()};J.object(kI);const AI=J.looseObject(kI);function jI(e){let t=k.toString(e);return MI(JSON.parse(t))}function MI(e){return AI.parse(e)}gS(),Px(),NN(),lO(),rO(),ZO(),LO();async function NI(e,t,n){try{n||console.log(X.dim(` Provisioning testnet wallet and funding via faucet...`));let t=he(),r=_e(t),i=j({chain:ye,transport:ue()});if(SD(e)){let{chainId:t,operator:n}=e.request.methodDetails??{};if(t===ye.id&&typeof n==`string`&&de(n)&&n!==me){let e=await Fe.faucet.fund(i,{account:n});await Promise.all(e.map(e=>Me(i,{hash:e})))}}let a=await Fe.faucet.fund(i,{account:r});await Promise.all(a.map(e=>Me(i,{hash:e}))),n||console.log(X.dim(` Using wallet: ${r.address}`));let o=e.request.currency;if(typeof o==`string`)for(let e=0;e<10;e++){let e=await Fe.token.getBalance(i,{account:r.address,token:o}).catch(()=>void 0);if(e&&e.amount>0n)break;await new Promise(e=>setTimeout(e,500))}return{methods:[...AN({account:r})]}}catch(e){t&&console.log(X.dim(` Provisioning failed: ${e.message}`));return}}async function PI(){let e=WO(),{isTempoAccount:t}=await Promise.resolve().then(()=>(LO(),uO)),{resolveTempoAccount:n}=await Promise.resolve().then(()=>(pP(),PN));if(t(e)){let t=n(e);if(t)return t.wallet_address}try{return(await JO()).address}catch{return}}function FI(e){try{let{execSync:t}=m,n=t(`stripe config --list`,{encoding:`utf8`,timeout:5e3}).match(/test_mode_api_key\s*=\s*'([^']+)'/);if(n?.[1])return e&&console.log(X.dim(` Using Stripe test key from stripe CLI`)),n[1]}catch{}}async function II(e,t,n){let r=j({chain:e,transport:ue()}),[i,a,o]=await Promise.all([n?De(r,{address:t,abi:se,functionName:`balanceOf`,args:[n]}):0n,De(r,{address:t,abi:se,functionName:`decimals`}),De(r,{address:t,abi:se,functionName:`symbol`}).catch(()=>void 0)]);return{balance:i,decimals:a,symbol:o??void 0}}function LI(e){return Object.values(Be).find(t=>t.id===e)}function RI(e,t){return`$${(Number(e)/10**t).toFixed(2)}`}function zI(e){switch(e.method){case Ax.evm:return`no EVM payment plugin yet`;case`solana`:return`no Solana payment plugin yet`;default:return`no plugin for ${e.method}/${e.intent}`}}async function BI(e,t,n,r){let i=[],a=cI(e,t,r.query),o=rI(r.extraHeaders),s;r.body&&(s=r.body,o[`content-type`]=`application/json`);let c;try{c=await XF(a,{method:t.method,headers:o,body:s??null})}catch(e){return i.push($(`Payment: fetch challenge`,e.message)),i}if(c.status!==402)return i.push(GF(`Payment: skipped`,`Endpoint returned ${c.status}`)),i;let l;try{l=sS(c)}catch(e){return i.push($(`Payment: parse challenge`,e.message)),i}if(l.length===0)return i.push($(`Payment: parse challenge`,`No Payment challenges in response`)),i;let u=[];bI(l,new URL(a).hostname,u);let d=u.filter(e=>e.severity===`fail`);if(d.length>0)return i.push(...d),r.onResults?.(d),i;let f=await TP().catch(()=>void 0),p=l.find(e=>{if(e.method!==Ax.tempo||CP(f?.config)?.some(t=>t.name===e.method&&t.intent===e.intent))return!1;let t=e.request.methodDetails;return typeof t?.chainId==`number`&&t.chainId!==iO.mainnet});if(p){let e=await NI(p,n,r.silent);if(e)try{await GI(i,await dT({methods:e.methods,polyfill:!1,async onChallenge(e,{createCredential:t}){DP(p,e);let n=await t(await yP(e,f?.config.extensions));return i.push(Q(`Payment: submitted`,`ephemeral testnet wallet`)),n},fetch:(e,t)=>XF(e,t??{},3e4)}).fetch(a,{method:t.method,headers:o,body:s??null}),n,ye)}catch(e){i.push($(`Payment: create credential`,e.message))}else i.push($(`Payment: auto-provision wallet`,`Failed to create and fund testnet wallet`));r.onResults?.(i)}let m=r.interactive,h=process.env.MPPX_STRIPE_SECRET_KEY??FI(n),g=h?.startsWith(`sk_test_`)||h?.startsWith(`rk_test_`),_=new Set([Ax.tempo,Ax.evm,Ax.stripe]);for(let e of l){if(e===p||!_.has(e.method))continue;r.silent||console.log(``);let c=i.length,l=()=>{r.onResults&&i.length>c&&r.onResults(i.slice(c))},u=`Payment [${e.method}]`;try{switch(e.method){case Ax.tempo:case Ax.evm:await VI(e,u,{results:i,url:a,endpoint:t,fetchHeaders:o,fetchBody:s,verbose:n,loaded:f,isInteractive:m,options:r});break;case Ax.stripe:await HI(e,u,{results:i,url:a,endpoint:t,fetchHeaders:o,fetchBody:s,verbose:n,loaded:f,isInteractive:m,isStripeTestKey:g??!1,stripeKey:h,options:r})}}finally{l()}}return i}async function VI(e,t,n){let{results:r,url:i,endpoint:a,fetchHeaders:o,fetchBody:s,verbose:c,loaded:l,options:u}=n,d=e.request,f=d.methodDetails,p=eI(d.amount)?BigInt(d.amount):void 0,m=f?.decimals??d.decimals??6,h=d.currency,{plugin:g,method:_}=bP(e,l?.config);if(!g&&!_){r.push(GF(t,zI(e)));return}let v;if(!_){try{v=await PI()}catch{}if(!v){r.push(GF(t,`no wallet configured. Run "mppx account create" to create one.`));return}}let y;if(e.method===Ax.tempo){let e=f?.chainId??(_?void 0:ve.id);e!==void 0&&(y=e===ye.id?ye:LI(e))}else if(e.method===Ax.evm){let e=f?.chainId;e&&(y=LI(e))}if(p&&h&&!y){r.push(GF(t,`unable to resolve payment chain and verify token decimals`));return}let b;if(p&&h&&y)try{let n;if(e.method===Ax.tempo){let e=await kO(j({chain:y,transport:ue()}),h,v??me,{requireDecimals:!0});n=e.balance,m=e.decimals,b=e.symbol}else{let e=await II(y,h,v);n=e.balance,m=e.decimals,b=e.symbol}if(v&&n<p){let e=RI(p,m),i=RI(n,m),a=b??`tokens`;r.push(GF(t,`insufficient balance (have ${i}, need ${e} ${a} on ${y.name})`,`Fund wallet ${v} with at least ${e} ${a} on ${y.name}.`));return}}catch(e){c&&console.log(X.dim(` Balance check skipped: ${e.message}`)),r.push(GF(t,`unable to verify token balance and decimals from the payment chain`));return}let x=p?RI(p,m):`unknown amount`,S=b??(h?.length===3?h.toUpperCase():``),C=y?.name,w=`${x}${S?` ${S}`:``}${C?` on ${C}`:``}`;if(u.silent||console.log(X.dim(v?` Attempting payment with wallet ${v}`:` Attempting payment with configured method`)),!_&&y?.testnet)u.silent||console.log(X.dim(` Auto-approved: ${w} (testnet)`));else if(!u.yes&&!n.isInteractive){r.push(GF(t,`non-interactive mode, use --yes to approve`));return}else if(u.yes)u.silent||console.log(X.dim(` Auto-approved: ${w}`));else if(!await xO(` ${X.yellow(`Pay`)} ${w}. Continue?`,!1)){r.push(GF(t,`declined`));return}let T,ee,E;if(g)try{let t=await g.setup({challenge:e,options:{network:`mainnet`},methodOpts:{}});T=t.methods,ee=t.createCredential,E=t.credentialContext}catch(e){r.push(GF(t,e.message));return}else T=[_];if(_&&SD(e)){let n=!0,u=dT({methods:[_],polyfill:!1,async onChallenge(n,{createCredential:i}){let a=await yP(n,l?.config.extensions);DP(e,n);let o=await i(a);return r.push(Q(`${t}: submitted`)),o},fetch:async(t,r)=>n?(n=!1,new Response(null,{status:402,headers:{[q.wwwAuthenticate]:Qx(e)}})):XF(t,r??{},3e4)});try{await GI(r,await u.fetch(i,{method:a.method,headers:o,body:s??null}),c,y)}catch(e){r.push($(t,e.message))}return}let D=await UI(e,T,ee,t,r,l?.config.extensions,E);D&&(g?.prepareCredentialRequest?.({challenge:e,credential:D,headers:o}),await WI(r,i,a,D,o,s,c,y,dS(e)))}async function HI(e,t,n){let{results:r,url:i,endpoint:a,fetchHeaders:o,fetchBody:s,verbose:c,loaded:l,options:u,stripeKey:d}=n,{plugin:f,method:p}=bP(e,l?.config);if(!f&&!p){r.push(GF(t,`no Stripe payment method available`));return}let m=!!(f&&!l?.config.plugins?.includes(f)&&n.isStripeTestKey),h=e.request,g=eI(h.amount)?BigInt(h.amount):void 0,_=h.decimals??2,v=h.currency,y=g?RI(g,_):`unknown amount`,b=v?.length===3?v.toUpperCase():``,x=`${y}${b?` ${b}`:``} via Stripe${m?` (testmode)`:``}`;if(m)u.silent||console.log(X.dim(` Attempting: ${x}`));else if(!u.yes&&!n.isInteractive){r.push(GF(t,`non-interactive mode, use --yes to approve`));return}else if(u.yes)u.silent||console.log(X.dim(` Auto-approved: ${x}`));else if(!await xO(` ${X.yellow(`Pay`)} ${x}. Continue?`,!1)){r.push(GF(t,`declined`));return}let S,C,w;if(f)try{let t={paymentMethod:`pm_card_visa`};d&&(t.secretKey=d);let n=await f.setup({challenge:e,options:{},methodOpts:t});S=n.methods,C=n.createCredential,w=n.credentialContext}catch(e){r.push(GF(t,e.message));return}else S=[p];let T=await UI(e,S,C,t,r,l?.config.extensions,w);if(T){if(f?.prepareCredentialRequest?.({challenge:e,credential:T,headers:o}),m){let n=await XF(i,{method:a.method,headers:{...o,[dS(e)]:T},body:s??null},3e4);n.status>=200&&n.status<300?r.push(Q(`${t}: successful`,`HTTP ${n.status}`)):r.push(GF(`${t}: server is in livemode`,void 0,`Run your server with a Stripe test key to automatically validate Stripe payments in testmode.`));return}await WI(r,i,a,T,o,s,c,void 0,dS(e))}}async function UI(e,t,n,r,i,a,o){let s=new Response(null,{status:402,headers:{[q.wwwAuthenticate]:Qx(e)}});try{let c=await yP(e,a,o),l;return l=n?await n(s,c):await dT({methods:t,polyfill:!1}).createCredential(s,c),i.push(Q(`${r}: submitted`)),l}catch(e){let t=e.message;t.toLowerCase().includes(`insufficient`)?i.push(GF(r,`insufficient balance: ${t}`)):i.push($(r,t));return}}async function WI(e,t,n,r,i,a,o,s,c=q.authorization){let l;try{l=await XF(t,{method:n.method,headers:{...i,[c]:r},body:a??null},3e4)}catch(t){return e.push($(`Payment: send credential`,t.message)),e}return GI(e,l,o,s)}async function GI(e,t,n,r){if(t.status===402){let n=await t.text().catch(()=>``),r=`Payment rejected`;try{let e=JSON.parse(n);r=e.detail??e.title??r}catch{}return e.push($(`Payment: accepted`,r,`The server rejected a valid credential. Check that your payment verification logic accepts the credential format and that the payment was processed on-chain.`)),e}if(t.status>=400&&t.status<500)e.push(WF(`Payment: post-payment response`,`Got ${t.status}`,`Payment succeeded but the endpoint returned a client error. The endpoint likely requires request body parameters. Use --body to provide them.`));else if(t.status>=500)return e.push($(`Payment: server response`,`Got ${t.status}`,`Payment was accepted but the server errored while generating the response. Check server logs for the underlying error.`)),e;else e.push(Q(`Payment: successful`,`HTTP ${t.status}`));let i=t.headers.get(q.paymentReceipt);if(!i)e.push($(`Payment-Receipt header present`,void 0,`After accepting payment, include a Payment-Receipt header with a base64url-encoded JSON object containing: method, reference, status ("success"), and timestamp (ISO 8601).`));else{e.push(Q(`Payment-Receipt header present`));try{let t=jI(i);if(e.push(Q(`Receipt parseable`)),t.status===`success`?e.push(Q(`Receipt status is "success"`)):e.push($(`Receipt status is "success"`,`Got: ${t.status}`)),t.reference){let n=/^0x[0-9a-fA-F]{64}$/.test(t.reference),r=t.reference.startsWith(`pi_`);n||r?e.push(Q(`Receipt reference valid`,t.reference.slice(0,20)+`...`)):e.push(WF(`Receipt reference format`,t.reference.slice(0,40)))}else e.push(WF(`Receipt has reference`,`No reference field`));if(t.timestamp){let n=new Date(t.timestamp),r=Date.now()-n.getTime();r<6e4?e.push(Q(`Receipt timestamp recent`,`${Math.round(r/1e3)}s ago`)):e.push(WF(`Receipt timestamp recent`,`${Math.round(r/6e4)}m ago`))}n&&console.log(X.dim(` Receipt: ${JSON.stringify(t,null,2)}`))}catch(t){e.push($(`Receipt parseable`,t.message))}}let a=t.headers.get(`content-type`)??``,o=await t.text().catch(()=>``);if(o.length>0)e.push(Q(`Response body non-empty`,`${a.split(`;`)[0]}, ${ZF(o.length)}`));else{let n=[...t.headers.entries()].filter(([e])=>!e.startsWith(`x-`)&&![`content-type`,`content-length`,`date`,`server`,`connection`,`keep-alive`,`cache-control`,`vary`,`access-control-allow-origin`,`payment-receipt`,`payment-session`,`payment-session-snapshot`].includes(e.toLowerCase()));n.length>0?e.push(WF(`Response body empty -- data may be in headers only`,n.map(([e])=>e).join(`, `))):e.push(WF(`Response body empty`))}if(a?e.push(Q(`Content-Type header set`,a.split(`;`)[0])):e.push(WF(`Content-Type header set`)),i)try{let t=jI(i);if(t.reference&&/^0x[0-9a-fA-F]{64}$/.test(t.reference)){let n=r?.blockExplorers?.default?.url;if(n){let r=n.includes(`tempo`)?`receipt`:`tx`;e.push(Q(`On-chain transaction`,`${n}/${r}/${t.reference}`))}}}catch{}return e}async function*KI(e){let t=e.url.replace(/\/$/,``).replace(/\/openapi\.json$/i,``),n=e.verbose??!1,r=await ZI(t,e);yield{phase:`discovery`,discovery:r,results:r.checks};for(let i of r.endpoints){yield{phase:`endpoint`,endpoint:i};let a,o=!1;e.endpoint?a=e.body:(a=nI(e.body,i.path),!a&&r.doc&&(a=sI(r.doc,i),o=!!a));let{results:s,resolvedBody:c,challenges:l}=await gI(t,i,n,{body:a,bodyIsAutogenerated:o,query:e.query,extraHeaders:e.headers,discoveryDoc:r.doc??void 0}),u=c??a,d=s.some(e=>e.severity===`pass`&&e.label===`Challenge parseable`),f=!s.some(e=>e.severity===`fail`),p=(l??[]).filter(e=>e.method!==`stripe`).map(e=>e.request.methodDetails?.chainId).filter(e=>typeof e==`number`),m=p.some(XI),h=p.some(e=>!XI(e)),g=!d&&s.some(e=>e.label===`Not an MPP endpoint`),_=s.some(e=>e.severity===`pass`&&e.label===`WWW-Authenticate header present`),v=s.some(e=>e.severity===`fail`&&e.label===`Challenge parseable`);if(yield{phase:`challenge`,endpoint:i,results:s,isMpp:d,isTestnet:m,isMainnet:h,isNonMppPayment:g,isMalformedChallenge:!d&&_&&v,methods:l?.map(e=>e.method)??[]},d&&(yield{phase:`errorHandling`,endpoint:i,results:await EI(t,i,{body:u,query:e.query})},!e.skipPayment&&f)){let r=e.onPaymentResults,a=await BI(t,i,n,{body:u,query:e.query,extraHeaders:e.headers,yes:e.yes,silent:!r,interactive:e.interactive,...r&&{onResults:r}});yield{phase:`payment`,endpoint:i,results:a,succeeded:a.some(e=>e.severity===`pass`&&e.label===`Payment: successful`)}}}}async function qI(e){let t=e.url.replace(/\/$/,``).replace(/\/openapi\.json$/i,``),n,r=[],i,a=!1,o=!1;for await(let t of KI(e))switch(t.phase){case`discovery`:n=t.discovery;break;case`endpoint`:i={method:t.endpoint.method,path:t.endpoint.path,challenge:[],errorHandling:[],payment:[]},r.push(i);break;case`challenge`:i&&(i.challenge=t.results),t.isMpp&&(t.isTestnet&&(a=!0),t.isMainnet&&(o=!0));break;case`errorHandling`:i&&(i.errorHandling=t.results);break;case`payment`:i&&(i.payment=t.results)}let s={passed:0,failed:0,warnings:0,suggested:0,skipped:0};if(n)for(let e of n.checks)e.severity===`pass`?s.passed++:e.severity===`fail`?s.failed++:e.severity===`warn`?s.warnings++:e.severity===`suggested`?s.suggested++:e.severity===`skip`&&s.skipped++;for(let e of r)for(let t of[...e.challenge,...e.errorHandling,...e.payment])t.severity===`pass`?s.passed++:t.severity===`fail`?s.failed++:t.severity===`warn`?s.warnings++:t.severity===`suggested`?s.suggested++:t.severity===`skip`&&s.skipped++;if(!n)throw Error(`Discovery phase did not complete`);let c=JI(r,n,{sawTestnet:a,sawMainnet:o});return{url:t,discovery:n,endpoints:r,summary:s,suggestions:c}}function JI(e,t,n){let r=[],i=e.flatMap(e=>[...e.challenge,...e.payment]);t.found||r.push(OI);let a=n.sawMainnet,o=n.sawTestnet,s=i.some(e=>e.label.includes(`[stripe]`)&&e.label.includes(`livemode`));return a&&!o?r.push(`Validate your testnet server to test without real funds (Tempo wallets are automatically provisioned).`):o&&!a&&r.push(`Validate your mainnet server to confirm real payments work end-to-end.`),s&&r.push(`Validate your testmode Stripe server to test card payments end-to-end (auto-pays with test card).`),r}const YI=Object.values(Be);function XI(e){return YI.find(t=>t.id===e)?.testnet===!0}async function ZI(e,t){let n=[],r=[],i=null,a=!1,o=!1;n.push(await aI(e));let s=new URL(e).origin,c;t.discoveryPath?c=[new URL(t.discoveryPath,e+`/`).href.replace(/\/openapi\.json$/i,``)]:(c=[e,s,`${s}/api`],c=[...new Set(c)]);let l,u=[];for(let e of c)if(l=await iI(e),`error`in l)u.push(`${e}/openapi.json: ${l.error}`);else break;if(!l||`error`in l)n.push({label:`Document found`,detail:u.join(`; `),hint:`MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.`,severity:`fail`});else{a=!0,n.push({label:`Document found and parseable`,severity:`pass`});let e=WT(l.doc),t=e.filter(e=>e.severity===`error`),r=e.filter(e=>e.severity===`warning`);if(t.length>0){n.push({label:`Valid OpenAPI structure`,detail:`${t.length} error(s)`,severity:`fail`});for(let e of t)n.push({label:e.message,detail:e.path,severity:`fail`})}else o=!0,n.push({label:`Valid OpenAPI structure`,severity:`pass`});for(let e of r)n.push({label:e.message,detail:e.path,severity:`warn`});i=l.doc}if(t.endpoint){let e=tI(t.endpoint);e&&r.push(e)}else if(i){r=oI(i);let e=BigInt(`999999999999999999`);r.sort((t,n)=>{let r=eI(t.amount)?BigInt(t.amount):e,i=eI(n.amount)?BigInt(n.amount):e;return r<i?-1:+(r>i)}),r.length>0?n.push({label:`Paid endpoints found`,detail:`${r.length} endpoint(s)`,severity:`pass`}):n.push({label:`Paid endpoints found`,detail:`No endpoints with x-payment-info`,severity:`warn`})}return{found:a,valid:o,endpoints:r,checks:n,doc:i}}Dx(),LO();const QI=sb(`validate`,{description:`Validate an MPP server implementation end-to-end`,args:p.object({url:p.string().describe(`Base URL of the MPP server to validate`)}),options:p.object({endpoint:p.string().optional().describe(`Endpoint to test (METHOD:path). Skips discovery.`),body:p.string().optional().describe(`Request body. With --endpoint, used directly. In discovery mode, JSON with / keys is a per-path mapping.`),query:p.array(p.string()).optional().describe(`Query parameter (key=value, repeatable)`),header:p.array(p.string()).optional().describe(`Request header (key:value, repeatable)`),verbose:p.number().default(0).meta({count:!0}).describe(`Verbosity level`),yes:p.boolean().default(!1).describe(`Auto-approve mainnet payments`),outputJson:p.boolean().default(!1).describe(`Output results as JSON (auto-enabled in known agent environments)`)}),alias:{endpoint:`e`,header:`H`,verbose:`v`,yes:`y`,outputJson:`j`},async run(e){if(e.options.outputJson||vO()){let t=await qI({url:e.args.url,endpoint:e.options.endpoint,body:e.options.body,query:e.options.query,headers:e.options.header,verbose:e.options.verbose>0,yes:e.options.yes,interactive:!1});console.log(JSON.stringify(t,null,2));let n=t.endpoints.length===0&&!e.options.endpoint;(t.summary.failed>0||!t.discovery.found||n)&&process.exit(1);return}let t=e.args.url.replace(/\/$/,``).replace(/\/openapi\.json$/i,``);console.log(`\n${X.bold(`mppx validate`)} ${X.dim(t)}\n`);let n={passed:0,failed:0,warnings:0,suggested:0,skipped:0},r=!1,i=!1,a=!1,o=!1,s=!1,c=!1,l=!1,u=0;for await(let t of KI({url:e.args.url,endpoint:e.options.endpoint,body:e.options.body,query:e.options.query,headers:e.options.header,verbose:e.options.verbose>0,yes:e.options.yes,interactive:!!process.stdin.isTTY,onPaymentResults:e=>YF(e,n)}))switch(t.phase){case`discovery`:if(JF(`Discovery (/openapi.json)`),YF(t.results,n),l=t.discovery.found,!l&&!e.options.endpoint){console.log(``);let[e,...t]=OI.split(`
|
|
245
|
-
`);console.log(X.yellow(` ${e}`));for(let e of t)console.log(e?X.dim(` ${e}`):``);console.log(``),process.exit(1)}t.discovery.endpoints.length===0&&!e.options.endpoint&&l&&(console.log(X.dim(` Use --endpoint to specify endpoints manually.`)),console.log(``),process.exit(1));break;case`endpoint`:u++,JF(`${t.endpoint.method} ${t.endpoint.path}`);break;case`challenge`:console.log(X.dim(` Challenge`)),YF(t.results,n),t.isMpp&&(r=!0,t.isTestnet&&(o=!0),t.isMainnet&&(s=!0)),t.isNonMppPayment&&(i=!0),t.isMalformedChallenge&&(a=!0);break;case`errorHandling`:console.log(X.dim(` Error Handling`)),YF(t.results,n);break;case`payment`:t.succeeded&&(c=!0)}$I(n,{sawMppEndpoint:r,sawNonMppPaymentEndpoint:i,sawMalformedChallenge:a,sawTestnet:o,sawMainnet:s,paymentSucceeded:c},u)}});function $I(e,t,n){!t.sawMppEndpoint&&n>0&&(console.log(``),t.sawMalformedChallenge?(console.log(X.yellow(` Payment scheme detected but challenge format is invalid on ${n} endpoint(s).`)),console.log(X.dim(` The server uses WWW-Authenticate: Payment but the challenge parameters do not conform to MPP.`)),console.log(X.dim(` Fix: encode payment details as base64url JSON in a request="..." parameter. See errors above.`))):t.sawNonMppPaymentEndpoint?(console.log(X.yellow(` No MPP endpoints found. Tested ${n} endpoint(s) but none use WWW-Authenticate: Payment.`)),console.log(X.dim(` This server may use x402 or another payment protocol.`))):e.skipped>0&&e.failed===0?(console.log(X.yellow(` Could not reach payment gate on any endpoint (all returned 401/403/200).`)),console.log(X.dim(` The server may require authentication before payment. Try providing auth or use --endpoint with a public path.`))):(console.log(X.yellow(` No MPP endpoints found. Tested ${n} endpoint(s) but none use WWW-Authenticate: Payment.`)),console.log(X.dim(` This server may use x402 or another payment protocol.`))),console.log(``),process.exit(1)),console.log(``);let r=[];e.passed>0&&r.push(X.green(`${e.passed} passed`)),e.failed>0&&r.push(X.red(`${e.failed} failed`)),e.warnings>0&&r.push(X.yellow(`${e.warnings} warning(s)`)),e.suggested>0&&r.push(X.cyan(`${e.suggested} suggested`)),e.skipped>0&&r.push(X.yellow(`${e.skipped} skipped`)),console.log(`${X.bold(`Summary:`)} ${r.join(`, `)}`),t.paymentSucceeded&&t.sawTestnet&&!t.sawMainnet?(console.log(``),console.log(X.dim(` Tip: also validate your mainnet server to confirm real payments work end-to-end.`))):t.sawMainnet&&!t.sawTestnet&&(console.log(``),console.log(X.dim(` Tip: also validate your testnet server for free. This CLI automatically provisions and funds a testnet wallet for testing.`))),console.log(``),e.failed>0&&process.exit(1)}function eL(e){return nL(e)?e.offers:[e]}function tL(e){let{_canonicalRequest:t,intent:n,name:r}=e,i=t.methodDetails??{},a=rL(t.amount)??rL(i.amount)??null,o=rL(t.currency)??rL(i.currency),s=rL(e.description)??rL(t.description),c={amount:a,...o?{currency:o}:{},...s?{description:s}:{},intent:n,method:r},l=new Set([`amount`,`currency`,`description`,`methodDetails`]);for(let[e,n]of Object.entries(t))!l.has(e)&&n!==void 0&&(c[e]=n);return c}function nL(e){return`offers`in e&&!(`_canonicalRequest`in e)}function rL(e){return typeof e==`string`?e:void 0}var iL=F((()=>{})),aL=Ze({generate:()=>oL,generateProxy:()=>sL});function oL(e,t){let n=e.methods,r=new Map,i={};for(let e of n)r.set(`${e.name}/${e.intent}`,e),i[e.intent]=(i[e.intent]??0)+1;for(let e of n)i[e.intent]===1&&r.set(e.intent,e);let a=t.routes.map(e=>lL(e,r));return cL({info:{title:t.info?.title??e.realm,version:t.info?.version??`1.0.0`},routes:a,serviceInfo:t.serviceInfo})}function sL(e){let t=e.routes.map(t=>({...t,path:uL(e.basePath,t.path)}));return cL({info:{title:e.info?.title??`API Proxy`,version:e.info?.version??`1.0.0`},routes:t,serviceInfo:e.serviceInfo})}function cL(e){let t={};for(let n of e.routes){let e=n.method.toLowerCase(),r={responses:{...n.payment?{402:{description:`Payment Required`}}:{},200:{description:`Successful response`}}};n.payment&&(r[`x-payment-info`]=LT.parse(n.payment)),n.summary&&(r.summary=n.summary),n.requestBody&&(r.requestBody=n.requestBody),t[n.path]||(t[n.path]={}),t[n.path][e]=r}let n={info:e.info,openapi:`3.1.0`,paths:t};return e.serviceInfo&&(n[`x-service-info`]=e.serviceInfo),n}function lL(e,t){if(`handler`in e){let t=e.handler._internal;if(!t)throw Error(`Route ${e.method.toUpperCase()} ${e.path} is missing discovery metadata`);return{method:e.method,path:e.path,payment:{offers:eL(t).map(tL)},...e.requestBody?{requestBody:e.requestBody}:{},...e.summary?{summary:e.summary}:{}}}let n=t.get(e.intent);if(!n)throw Error(`Unknown intent "${e.intent}" for route ${e.method.toUpperCase()} ${e.path}. Available: ${[...t.keys()].join(`, `)}`);let{description:r,...i}=e.options,a={_canonicalRequest:Ix(n,i),...typeof r==`string`?{description:r}:{},intent:n.intent,name:n.name};return{method:e.method,path:e.path,payment:tL(a),...e.requestBody?{requestBody:e.requestBody}:{},...e.summary?{summary:e.summary}:{}}}function uL(e,t){if(!e)return t;let n=e.startsWith(`/`)?e:`/${e}`;return`${n.endsWith(`/`)?n.slice(0,-1):n}${t}`}var dL=F((()=>{Rx(),UT(),iL()}));Dx(),gS(),oT(),UC(),nw(),aw(),Px(),rO(),ew(),ES(),DC(),ZO(),LO();const fL=`0.13.2`,pL=p.object({address:p.string(),isDefault:p.boolean().optional(),name:p.string(),source:p.string().optional()}),mL=p.object({address:p.string(),balances:p.array(p.string()).optional(),name:p.string(),type:p.string().optional()}),hL=p.object({message:p.string(),path:p.string(),severity:p.string()}),gL=p.object({description:p.string().optional(),id:p.string(),name:p.string().optional(),paidEndpoints:p.number(),status:p.string().optional(),url:p.string().optional()}),_L=p.object({description:p.string().optional(),method:p.string(),path:p.string(),payment:p.unknown().optional()});function vL(e){return e.format===`json`&&e.formatExplicit}function yL(e,t,n){if(vL(e))return e.ok(t);n()}async function bL(t){if(!t.body)return;let n=t.body.getReader();try{for(;;){let{done:t,value:r}=await n.read();if(t)break;process.stdout.write(Buffer.from(r))||await e(process.stdout,`drain`)}}finally{n.releaseLock()}}function xL(){return process.stdin.isTTY===!1&&process.stdin.listenerCount(`data`)===0&&process.stdin.listenerCount(`readable`)===0}function SL(e){return typeof e==`string`?e:void 0}function CL(e){return Array.isArray(e.endpoints)?e.endpoints.filter(e=>typeof e==`object`&&!!e):[]}function wL(e){let t=CL(e);return{...SL(e.description)?{description:SL(e.description)}:{},id:SL(e.id)??SL(e.name)??`unknown`,...SL(e.name)?{name:SL(e.name)}:{},paidEndpoints:t.filter(e=>e.payment).length,...SL(e.status)?{status:SL(e.status)}:{},...SL(e.serviceUrl)||SL(e.url)?{url:SL(e.serviceUrl)??SL(e.url)}:{}}}function TL(e){return{...SL(e.description)?{description:SL(e.description)}:{},method:SL(e.method)??`GET`,path:SL(e.path)??`/`,...e.payment===void 0?{}:{payment:e.payment}}}function EL(e){if(!e)return`free`;if(typeof e!=`object`)return String(e);let t=e,n=SL(t.amount),r=SL(t.currency),i=SL(t.method),a=SL(t.intent);return[n,r,i&&a?`${i}/${a}`:i??a].filter(Boolean).join(` `)}function DL(e){return e.request.currency??e.request.asset}function OL(e,t){if(!t)return[...e];let n=t.toLowerCase();return e.filter(e=>{let t=DL(e);return typeof t==`string`&&t.toLowerCase()===n})}async function kL(e,t,n={}){let r=()=>HC().getChallenges(e),i=()=>tw().getChallenges(n.ignoreX402ResponseUrl?new Response(null,{headers:e.headers,status:e.status}):e);if(t===`mpp`)return r();if(t===`x402`)return i();let a=[],o;try{a=await r()}catch(e){o=e}let s=[],c;try{s=await i()}catch(e){c=e}if(a.length||s.length)return[...a,...s];if(o)throw o;if(c)throw c;return[]}function AL(e){return rw({name:`selected-challenge`,isPaymentRequired:e=>e.status===402,getChallenges:()=>[e],setCredential:e=>e})}function jL(e,t){if(!wS(e))return new Response(null,{status:402,headers:{[q.wwwAuthenticate]:Qx(e)}});let n=e.request;return n.resource?new Response(null,{status:402,headers:{[lC]:ZC({accepts:[iC(n)],...n.extensions&&{extensions:n.extensions},resource:n.resource,x402Version:2})}}):t}function ML(e){let{host:t,hostname:n}=new URL(e);return!n.endsWith(`.localhost`)||n===`localhost`?{url:e}:{host:t,url:e.replace(n,`127.0.0.1`)}}async function NL(e,t,n,r){let i=t,a=ML(i),o={...n,redirect:`manual`,headers:Sw(n.headers)};a.host&&IL(o.headers,a.host);for(let t=0;t<=20;t++){let n=await e(a.url,o),s=n.headers.get(`location`);if(!r||!s||![301,302,303,307,308].includes(n.status))return{init:o,logicalUrl:i,response:n,target:a};if(t===20)throw TypeError(`Too many redirects`);await n.body?.cancel().catch(()=>{});let c=new URL(s,i),l=new URL(i),u=Sw(o.headers);if(c.origin!==l.origin)for(let e of[`authorization`,`cookie`,`host`,`proxy-authorization`])for(let t of Object.keys(u))t.toLowerCase()===e&&delete u[t];let d=(o.method??(o.body?`POST`:`GET`)).toUpperCase(),f=n.status===303&&d!==`GET`&&d!==`HEAD`||(n.status===301||n.status===302)&&d===`POST`;if(f)for(let e of[`content-encoding`,`content-language`,`content-length`,`content-location`,`content-type`])for(let t of Object.keys(u))t.toLowerCase()===e&&delete u[t];i=c.href,a=ML(i),a.host&&IL(u,a.host);let p={...o};f&&(delete p.body,p.method=`GET`),o={...p,headers:u,redirect:`manual`}}throw TypeError(`Too many redirects`)}function PL(e,t,n){let r=new URL(e),i=new URL(n||e);return i.origin!==r.origin&&FL(i),ML(i.href)}function FL(e){throw new I({code:`UNSAFE_PAYMENT_REDIRECT`,message:`Refusing to send a payment credential to redirected origin ${e.origin}.`,exitCode:1})}function IL(e,t){for(let t of Object.keys(e))t.toLowerCase()===`host`&&delete e[t];t&&(e.Host=t)}function LL(e){let t=e.request.resource;return typeof t?.url==`string`?t.url:void 0}async function RL(){let e=process.env.MPPX_SERVICES_URL??`https://mpp.dev/api/services`,t=await globalThis.fetch(e);if(!t.ok)throw new I({code:`SERVICES_FETCH_FAILED`,message:`Failed to fetch services registry: HTTP ${t.status}`,exitCode:1});let n=await t.json();if(!n||typeof n!=`object`||!Array.isArray(n.services))throw new I({code:`SERVICES_INVALID`,message:`Services registry response did not contain a services array.`,exitCode:1});return n.services}function zL(e,t){let n=t.toLowerCase();return e.find(e=>{let t=SL(e.id)?.toLowerCase(),r=SL(e.name)?.toLowerCase();return t===n||r===n})}const BL=sb(`mppx`,{mcp:{tools:{discovery:`direct`}},version:fL,description:`Make HTTP requests with automatic payment handling`,usage:[{suffix:`<url> [options]`}],args:p.object({url:p.string().describe(`URL to make request to`)}),options:p.object({account:p.string().optional().describe(`Account name (env: MPPX_ACCOUNT)`),autoSwap:p.boolean().optional().describe(`Auto-swap source tokens into payment currency`),config:p.string().optional().describe(`Path to config file`),confirm:p.boolean().optional().default(!1).describe(`Show confirmation prompts`),currency:p.string().optional().describe(`Payment currency/token address to select`),data:p.string().optional().describe(`Send request body (implies POST unless -X is set)`),fail:p.boolean().optional().describe(`Fail silently on HTTP errors (exit 22)`),header:p.array(p.string()).optional().describe(`Add header (repeatable)`),include:p.boolean().optional().describe(`Include response headers in output`),insecure:p.boolean().optional().describe(`Skip TLS certificate verification (true for localhost/.local)`),jsonBody:p.string().optional().describe(`Send JSON body (sets Content-Type and Accept, implies POST)`),location:p.boolean().optional().describe(`Follow redirects`),method:p.string().optional().describe(`HTTP method`),methodOpt:p.array(p.string()).optional().describe(`Method-specific option (key=value, repeatable)`),network:p.enum([`mainnet`,`testnet`]).optional().describe(`Tempo network`),payWith:p.string().optional().describe(`Source token for Tempo auto-swap`),protocol:p.enum([`auto`,`mpp`,`x402`]).default(`auto`).describe(`Payment protocol to use`),rpcUrl:p.string().optional().describe(`RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)`),session:p.string().optional().default(`auto`).refine(e=>e===`auto`||e===`new`||/^0x[0-9a-fA-F]{64}$/.test(e),`Expected auto, new, or a 32-byte channel ID`).describe(`Session selection: auto, new, or channel ID`),silent:p.boolean().default(!1).describe(`Silent mode (suppress progress and info)`),slippage:p.number().optional().describe(`Tempo auto-swap max slippage percentage`),userAgent:p.string().optional().describe(`Set User-Agent header`),verbose:p.number().default(0).meta({count:!0}).describe(`Verbosity (-v details, -vv headers)`)}),alias:{account:`a`,config:`c`,data:`d`,fail:`f`,header:`H`,include:`i`,insecure:`k`,jsonBody:`J`,location:`L`,method:`X`,methodOpt:`M`,rpcUrl:`r`,silent:`s`,userAgent:`A`,verbose:`v`},examples:[{args:{url:`mpp.dev/api/ping/paid`},description:`Make a payment request`}],async run(e){let t=e.options.silent?e=>{}:e=>process.stderr.write(e),n=await TP(e.options.config);n&&e.options.verbose>=1&&t(`${X.dim(`Using config`)} ${X.blue(c.relative(process.cwd(),n.path))}\n`);let r=$O(),i={"User-Agent":e.options.userAgent??`mppx/${fL}${r?` AIAgent/${r}`:``}`};if(e.options.header)for(let t of e.options.header){let n=t.indexOf(`:`);if(n===-1)return e.error({code:`INVALID_HEADER`,message:`Invalid header format: ${t}`,exitCode:2});i[t.slice(0,n).trim()]=t.slice(n+1).trim()}let a=SP(n?.config);a&&!Object.keys(i).some(e=>e.toLowerCase()===`accept-payment`)&&(i[`Accept-Payment`]=a);let o=(()=>{let t=/^https?:\/\//.test(e.args.url),n=/^(localhost|.*\.localhost|127\.0\.0\.1|\[::1\])(:\d+)?/.test(e.args.url);return t?e.args.url:`${n?`http`:`https`}://${e.args.url}`})(),{hostname:s}=new URL(o),l=e.options.insecure||s===`localhost`||s.endsWith(`.localhost`)||s.endsWith(`.local`)?async(e,t)=>{let n=process.env.NODE_TLS_REJECT_UNAUTHORIZED;process.env.NODE_TLS_REJECT_UNAUTHORIZED=`0`;try{return await globalThis.fetch(e,t)}finally{n===void 0?delete process.env.NODE_TLS_REJECT_UNAUTHORIZED:process.env.NODE_TLS_REJECT_UNAUTHORIZED=n}}:globalThis.fetch,u=ML(o),d=u.url;u.host&&(i.Host=u.host);try{let r=_O(e.options.methodOpt),a=HP(),s={redirect:e.options.location?`follow`:`manual`};e.options.jsonBody?(s.body=e.options.jsonBody,i[`Content-Type`]??=`application/json`,i.Accept??=`application/json`):e.options.data&&(s.body=e.options.data),e.options.method?s.method=e.options.method.toUpperCase():s.body&&(s.method=`POST`),Object.keys(i).length>0&&(s.headers=i);let c={include:e.options.include??!1,verbose:e.options.verbose,silent:e.options.silent};e.options.verbose>=2&&dO(o,s,t);let f=await NL(l,o,s,e.options.location??!1),p=f.response;if(p.status!==402){if(e.options.session!==`auto`)return e.error({code:`UNSUPPORTED_SESSION`,message:`--session requires a tempo/session payment challenge.`,exitCode:2});if(e.options.fail&&p.status>=400)return e.error({code:`HTTP_ERROR`,message:`HTTP error ${p.status}`,exitCode:22});fO(p,c),await bL(p);return}let m=p.url?new URL(p.url):void 0,h=u.host!==void 0&&m?.hostname===`127.0.0.1`,g=await kL(p,e.options.protocol,{ignoreX402ResponseUrl:h});if(g.length===0)return e.error({code:`UNSUPPORTED_PROTOCOL`,message:e.options.protocol===`auto`?`Server did not offer a payment challenge.`:`Server did not offer an ${e.options.protocol} payment challenge.`,exitCode:2});let _=OL(g,e.options.currency);if(e.options.currency&&_.length===0){let t=g.map(DL).filter(e=>typeof e==`string`).join(`, `);return e.error({code:`UNSUPPORTED_CURRENCY`,message:`Server did not offer payment currency ${e.options.currency}.${t?` Offered currencies: ${t}.`:``}`,exitCode:2})}let v=CP(n?.config)?.length?_:await IN(_,{options:{account:e.options.account,network:e.options.network,rpcUrl:e.options.rpcUrl}}),y=xP(v,n?.config);if(!y){let t=v.map(e=>`${e.method}/${e.intent}`).join(`, `);return e.error({code:`UNSUPPORTED_METHOD`,message:`Unsupported payment method. Server offers: ${t}. Add it to mppx.config.ts using defineConfig().`,exitCode:2})}let{challenge:b,plugin:x,method:S}=y,C=wS(b),w=C?void 0:PL(o,u,f.logicalUrl),T=jL(b,p),ee=b.request.currency??``,E=b.request.decimals??6,D,O;x&&(O=await x.setup({challenge:b,options:{account:e.options.account,autoSwap:e.options.autoSwap,network:e.options.network,payWith:e.options.payWith,rpcUrl:e.options.rpcUrl,slippage:e.options.slippage},methodOpts:r}),ee=O.tokenSymbol,E=O.tokenDecimals,D=O.explorerUrl);let te=!e.options.silent&&e.options.confirm,ne=new Set;{fO(p,b.method===`tempo`&&b.intent===`session`&&e.options.include?{...c,include:!1,verbose:Math.max(c.verbose,2)}:c);let n=(()=>{let e=new Set([`id`,`request`]),t=[];for(let[n,r]of Object.entries(b))e.has(n)||r===void 0||t.push([n,hO(n,r)]);return t.sort(([e],[t])=>e.localeCompare(t))})(),r={tokenSymbol:ee,tokenDecimals:E,explorerUrl:D},i=(()=>{let e=new Set([`decimals`,`currency`,`methodDetails`]),t=[];for(let[n,i]of Object.entries(b.request))e.has(n)||i===void 0||t.push([n,pO(n,i,r)]);return t.sort(([e],[t])=>e.localeCompare(t))})(),a=(()=>{let e=b.request.methodDetails;if(!e)return[];let t=[];for(let[n,i]of Object.entries(e))if(i!==void 0){if(n===`memo`&&typeof i==`string`){let e=mO(i);t.push([n,e?`${e}\n${X.dim(i)}`:i])}else t.push([n,pO(n,i,r)])}return t.sort(([e],[t])=>e.localeCompare(t))})(),o=[[`Challenge`,n],[`Request`,i],...a.length?[[`Details`,a]]:[]];for(let[,e]of o)for(let[t]of e)ne.add(t);let s=Math.max(...o.flatMap(([,e])=>e.map(([e])=>e.length))),l=` ${``.padEnd(s)} `;if(e.options.verbose>=1||te){t(`${X.bold(X.yellow(`Payment Required`))}\n`);for(let[e,n]of o){t(`${X.bold(e)}\n`);for(let[e,r]of n){let[n,...i]=r.split(`
|
|
245
|
+
`);console.log(X.yellow(` ${e}`));for(let e of t)console.log(e?X.dim(` ${e}`):``);console.log(``),process.exit(1)}t.discovery.endpoints.length===0&&!e.options.endpoint&&l&&(console.log(X.dim(` Use --endpoint to specify endpoints manually.`)),console.log(``),process.exit(1));break;case`endpoint`:u++,JF(`${t.endpoint.method} ${t.endpoint.path}`);break;case`challenge`:console.log(X.dim(` Challenge`)),YF(t.results,n),t.isMpp&&(r=!0,t.isTestnet&&(o=!0),t.isMainnet&&(s=!0)),t.isNonMppPayment&&(i=!0),t.isMalformedChallenge&&(a=!0);break;case`errorHandling`:console.log(X.dim(` Error Handling`)),YF(t.results,n);break;case`payment`:t.succeeded&&(c=!0)}$I(n,{sawMppEndpoint:r,sawNonMppPaymentEndpoint:i,sawMalformedChallenge:a,sawTestnet:o,sawMainnet:s,paymentSucceeded:c},u)}});function $I(e,t,n){!t.sawMppEndpoint&&n>0&&(console.log(``),t.sawMalformedChallenge?(console.log(X.yellow(` Payment scheme detected but challenge format is invalid on ${n} endpoint(s).`)),console.log(X.dim(` The server uses WWW-Authenticate: Payment but the challenge parameters do not conform to MPP.`)),console.log(X.dim(` Fix: encode payment details as base64url JSON in a request="..." parameter. See errors above.`))):t.sawNonMppPaymentEndpoint?(console.log(X.yellow(` No MPP endpoints found. Tested ${n} endpoint(s) but none use WWW-Authenticate: Payment.`)),console.log(X.dim(` This server may use x402 or another payment protocol.`))):e.skipped>0&&e.failed===0?(console.log(X.yellow(` Could not reach payment gate on any endpoint (all returned 401/403/200).`)),console.log(X.dim(` The server may require authentication before payment. Try providing auth or use --endpoint with a public path.`))):(console.log(X.yellow(` No MPP endpoints found. Tested ${n} endpoint(s) but none use WWW-Authenticate: Payment.`)),console.log(X.dim(` This server may use x402 or another payment protocol.`))),console.log(``),process.exit(1)),console.log(``);let r=[];e.passed>0&&r.push(X.green(`${e.passed} passed`)),e.failed>0&&r.push(X.red(`${e.failed} failed`)),e.warnings>0&&r.push(X.yellow(`${e.warnings} warning(s)`)),e.suggested>0&&r.push(X.cyan(`${e.suggested} suggested`)),e.skipped>0&&r.push(X.yellow(`${e.skipped} skipped`)),console.log(`${X.bold(`Summary:`)} ${r.join(`, `)}`),t.paymentSucceeded&&t.sawTestnet&&!t.sawMainnet?(console.log(``),console.log(X.dim(` Tip: also validate your mainnet server to confirm real payments work end-to-end.`))):t.sawMainnet&&!t.sawTestnet&&(console.log(``),console.log(X.dim(` Tip: also validate your testnet server for free. This CLI automatically provisions and funds a testnet wallet for testing.`))),console.log(``),e.failed>0&&process.exit(1)}function eL(e){return nL(e)?e.offers:[e]}function tL(e){let{_canonicalRequest:t,intent:n,name:r}=e,i=t.methodDetails??{},a=rL(t.amount)??rL(i.amount)??null,o=rL(t.currency)??rL(i.currency),s=rL(e.description)??rL(t.description),c={amount:a,...o?{currency:o}:{},...s?{description:s}:{},intent:n,method:r},l=new Set([`amount`,`currency`,`description`,`methodDetails`]);for(let[e,n]of Object.entries(t))!l.has(e)&&n!==void 0&&(c[e]=n);return c}function nL(e){return`offers`in e&&!(`_canonicalRequest`in e)}function rL(e){return typeof e==`string`?e:void 0}var iL=F((()=>{})),aL=Ze({generate:()=>oL,generateProxy:()=>sL});function oL(e,t){let n=e.methods,r=new Map,i={};for(let e of n)r.set(`${e.name}/${e.intent}`,e),i[e.intent]=(i[e.intent]??0)+1;for(let e of n)i[e.intent]===1&&r.set(e.intent,e);let a=t.routes.map(e=>lL(e,r));return cL({info:{title:t.info?.title??e.realm,version:t.info?.version??`1.0.0`},routes:a,serviceInfo:t.serviceInfo})}function sL(e){let t=e.routes.map(t=>({...t,path:uL(e.basePath,t.path)}));return cL({info:{title:e.info?.title??`API Proxy`,version:e.info?.version??`1.0.0`},routes:t,serviceInfo:e.serviceInfo})}function cL(e){let t={};for(let n of e.routes){let e=n.method.toLowerCase(),r={responses:{...n.payment?{402:{description:`Payment Required`}}:{},200:{description:`Successful response`}}};n.payment&&(r[`x-payment-info`]=LT.parse(n.payment)),n.summary&&(r.summary=n.summary),n.requestBody&&(r.requestBody=n.requestBody),t[n.path]||(t[n.path]={}),t[n.path][e]=r}let n={info:e.info,openapi:`3.1.0`,paths:t};return e.serviceInfo&&(n[`x-service-info`]=e.serviceInfo),n}function lL(e,t){if(`handler`in e){let t=e.handler._internal;if(!t)throw Error(`Route ${e.method.toUpperCase()} ${e.path} is missing discovery metadata`);return{method:e.method,path:e.path,payment:{offers:eL(t).map(tL)},...e.requestBody?{requestBody:e.requestBody}:{},...e.summary?{summary:e.summary}:{}}}let n=t.get(e.intent);if(!n)throw Error(`Unknown intent "${e.intent}" for route ${e.method.toUpperCase()} ${e.path}. Available: ${[...t.keys()].join(`, `)}`);let{description:r,...i}=e.options,a={_canonicalRequest:Ix(n,i),...typeof r==`string`?{description:r}:{},intent:n.intent,name:n.name};return{method:e.method,path:e.path,payment:tL(a),...e.requestBody?{requestBody:e.requestBody}:{},...e.summary?{summary:e.summary}:{}}}function uL(e,t){if(!e)return t;let n=e.startsWith(`/`)?e:`/${e}`;return`${n.endsWith(`/`)?n.slice(0,-1):n}${t}`}var dL=F((()=>{Rx(),UT(),iL()}));Dx(),gS(),oT(),UC(),nw(),aw(),Px(),rO(),ew(),ES(),DC(),ZO(),LO();const fL=`0.13.3`,pL=p.object({address:p.string(),isDefault:p.boolean().optional(),name:p.string(),source:p.string().optional()}),mL=p.object({address:p.string(),balances:p.array(p.string()).optional(),name:p.string(),type:p.string().optional()}),hL=p.object({message:p.string(),path:p.string(),severity:p.string()}),gL=p.object({description:p.string().optional(),id:p.string(),name:p.string().optional(),paidEndpoints:p.number(),status:p.string().optional(),url:p.string().optional()}),_L=p.object({description:p.string().optional(),method:p.string(),path:p.string(),payment:p.unknown().optional()});function vL(e){return e.format===`json`&&e.formatExplicit}function yL(e,t,n){if(vL(e))return e.ok(t);n()}async function bL(t){if(!t.body)return;let n=t.body.getReader();try{for(;;){let{done:t,value:r}=await n.read();if(t)break;process.stdout.write(Buffer.from(r))||await e(process.stdout,`drain`)}}finally{n.releaseLock()}}function xL(){return process.stdin.isTTY===!1&&process.stdin.listenerCount(`data`)===0&&process.stdin.listenerCount(`readable`)===0}function SL(e){return typeof e==`string`?e:void 0}function CL(e){return Array.isArray(e.endpoints)?e.endpoints.filter(e=>typeof e==`object`&&!!e):[]}function wL(e){let t=CL(e);return{...SL(e.description)?{description:SL(e.description)}:{},id:SL(e.id)??SL(e.name)??`unknown`,...SL(e.name)?{name:SL(e.name)}:{},paidEndpoints:t.filter(e=>e.payment).length,...SL(e.status)?{status:SL(e.status)}:{},...SL(e.serviceUrl)||SL(e.url)?{url:SL(e.serviceUrl)??SL(e.url)}:{}}}function TL(e){return{...SL(e.description)?{description:SL(e.description)}:{},method:SL(e.method)??`GET`,path:SL(e.path)??`/`,...e.payment===void 0?{}:{payment:e.payment}}}function EL(e){if(!e)return`free`;if(typeof e!=`object`)return String(e);let t=e,n=SL(t.amount),r=SL(t.currency),i=SL(t.method),a=SL(t.intent);return[n,r,i&&a?`${i}/${a}`:i??a].filter(Boolean).join(` `)}function DL(e){return e.request.currency??e.request.asset}function OL(e,t){if(!t)return[...e];let n=t.toLowerCase();return e.filter(e=>{let t=DL(e);return typeof t==`string`&&t.toLowerCase()===n})}async function kL(e,t,n={}){let r=()=>HC().getChallenges(e),i=()=>tw().getChallenges(n.ignoreX402ResponseUrl?new Response(null,{headers:e.headers,status:e.status}):e);if(t===`mpp`)return r();if(t===`x402`)return i();let a=[],o;try{a=await r()}catch(e){o=e}let s=[],c;try{s=await i()}catch(e){c=e}if(a.length||s.length)return[...a,...s];if(o)throw o;if(c)throw c;return[]}function AL(e){return rw({name:`selected-challenge`,isPaymentRequired:e=>e.status===402,getChallenges:()=>[e],setCredential:e=>e})}function jL(e,t){if(!wS(e))return new Response(null,{status:402,headers:{[q.wwwAuthenticate]:Qx(e)}});let n=e.request;return n.resource?new Response(null,{status:402,headers:{[lC]:ZC({accepts:[iC(n)],...n.extensions&&{extensions:n.extensions},resource:n.resource,x402Version:2})}}):t}function ML(e){let{host:t,hostname:n}=new URL(e);return!n.endsWith(`.localhost`)||n===`localhost`?{url:e}:{host:t,url:e.replace(n,`127.0.0.1`)}}async function NL(e,t,n,r){let i=t,a=ML(i),o={...n,redirect:`manual`,headers:Sw(n.headers)};a.host&&IL(o.headers,a.host);for(let t=0;t<=20;t++){let n=await e(a.url,o),s=n.headers.get(`location`);if(!r||!s||![301,302,303,307,308].includes(n.status))return{init:o,logicalUrl:i,response:n,target:a};if(t===20)throw TypeError(`Too many redirects`);await n.body?.cancel().catch(()=>{});let c=new URL(s,i),l=new URL(i),u=Sw(o.headers);if(c.origin!==l.origin)for(let e of[`authorization`,`cookie`,`host`,`proxy-authorization`])for(let t of Object.keys(u))t.toLowerCase()===e&&delete u[t];let d=(o.method??(o.body?`POST`:`GET`)).toUpperCase(),f=n.status===303&&d!==`GET`&&d!==`HEAD`||(n.status===301||n.status===302)&&d===`POST`;if(f)for(let e of[`content-encoding`,`content-language`,`content-length`,`content-location`,`content-type`])for(let t of Object.keys(u))t.toLowerCase()===e&&delete u[t];i=c.href,a=ML(i),a.host&&IL(u,a.host);let p={...o};f&&(delete p.body,p.method=`GET`),o={...p,headers:u,redirect:`manual`}}throw TypeError(`Too many redirects`)}function PL(e,t,n){let r=new URL(e),i=new URL(n||e);return i.origin!==r.origin&&FL(i),ML(i.href)}function FL(e){throw new I({code:`UNSAFE_PAYMENT_REDIRECT`,message:`Refusing to send a payment credential to redirected origin ${e.origin}.`,exitCode:1})}function IL(e,t){for(let t of Object.keys(e))t.toLowerCase()===`host`&&delete e[t];t&&(e.Host=t)}function LL(e){let t=e.request.resource;return typeof t?.url==`string`?t.url:void 0}async function RL(){let e=process.env.MPPX_SERVICES_URL??`https://mpp.dev/api/services`,t=await globalThis.fetch(e);if(!t.ok)throw new I({code:`SERVICES_FETCH_FAILED`,message:`Failed to fetch services registry: HTTP ${t.status}`,exitCode:1});let n=await t.json();if(!n||typeof n!=`object`||!Array.isArray(n.services))throw new I({code:`SERVICES_INVALID`,message:`Services registry response did not contain a services array.`,exitCode:1});return n.services}function zL(e,t){let n=t.toLowerCase();return e.find(e=>{let t=SL(e.id)?.toLowerCase(),r=SL(e.name)?.toLowerCase();return t===n||r===n})}const BL=sb(`mppx`,{mcp:{tools:{discovery:`direct`}},version:fL,description:`Make HTTP requests with automatic payment handling`,usage:[{suffix:`<url> [options]`}],args:p.object({url:p.string().describe(`URL to make request to`)}),options:p.object({account:p.string().optional().describe(`Account name (env: MPPX_ACCOUNT)`),autoSwap:p.boolean().optional().describe(`Auto-swap source tokens into payment currency`),config:p.string().optional().describe(`Path to config file`),confirm:p.boolean().optional().default(!1).describe(`Show confirmation prompts`),currency:p.string().optional().describe(`Payment currency/token address to select`),data:p.string().optional().describe(`Send request body (implies POST unless -X is set)`),fail:p.boolean().optional().describe(`Fail silently on HTTP errors (exit 22)`),header:p.array(p.string()).optional().describe(`Add header (repeatable)`),include:p.boolean().optional().describe(`Include response headers in output`),insecure:p.boolean().optional().describe(`Skip TLS certificate verification (true for localhost/.local)`),jsonBody:p.string().optional().describe(`Send JSON body (sets Content-Type and Accept, implies POST)`),location:p.boolean().optional().describe(`Follow redirects`),method:p.string().optional().describe(`HTTP method`),methodOpt:p.array(p.string()).optional().describe(`Method-specific option (key=value, repeatable)`),network:p.enum([`mainnet`,`testnet`]).optional().describe(`Tempo network`),payWith:p.string().optional().describe(`Source token for Tempo auto-swap`),protocol:p.enum([`auto`,`mpp`,`x402`]).default(`auto`).describe(`Payment protocol to use`),rpcUrl:p.string().optional().describe(`RPC endpoint, defaults to public RPC for chain (env: MPPX_RPC_URL)`),session:p.string().optional().default(`auto`).refine(e=>e===`auto`||e===`new`||/^0x[0-9a-fA-F]{64}$/.test(e),`Expected auto, new, or a 32-byte channel ID`).describe(`Session selection: auto, new, or channel ID`),silent:p.boolean().default(!1).describe(`Silent mode (suppress progress and info)`),slippage:p.number().optional().describe(`Tempo auto-swap max slippage percentage`),userAgent:p.string().optional().describe(`Set User-Agent header`),verbose:p.number().default(0).meta({count:!0}).describe(`Verbosity (-v details, -vv headers)`)}),alias:{account:`a`,config:`c`,data:`d`,fail:`f`,header:`H`,include:`i`,insecure:`k`,jsonBody:`J`,location:`L`,method:`X`,methodOpt:`M`,rpcUrl:`r`,silent:`s`,userAgent:`A`,verbose:`v`},examples:[{args:{url:`mpp.dev/api/ping/paid`},description:`Make a payment request`}],async run(e){let t=e.options.silent?e=>{}:e=>process.stderr.write(e),n=await TP(e.options.config);n&&e.options.verbose>=1&&t(`${X.dim(`Using config`)} ${X.blue(c.relative(process.cwd(),n.path))}\n`);let r=$O(),i={"User-Agent":e.options.userAgent??`mppx/${fL}${r?` AIAgent/${r}`:``}`};if(e.options.header)for(let t of e.options.header){let n=t.indexOf(`:`);if(n===-1)return e.error({code:`INVALID_HEADER`,message:`Invalid header format: ${t}`,exitCode:2});i[t.slice(0,n).trim()]=t.slice(n+1).trim()}let a=SP(n?.config);a&&!Object.keys(i).some(e=>e.toLowerCase()===`accept-payment`)&&(i[`Accept-Payment`]=a);let o=(()=>{let t=/^https?:\/\//.test(e.args.url),n=/^(localhost|.*\.localhost|127\.0\.0\.1|\[::1\])(:\d+)?/.test(e.args.url);return t?e.args.url:`${n?`http`:`https`}://${e.args.url}`})(),{hostname:s}=new URL(o),l=e.options.insecure||s===`localhost`||s.endsWith(`.localhost`)||s.endsWith(`.local`)?async(e,t)=>{let n=process.env.NODE_TLS_REJECT_UNAUTHORIZED;process.env.NODE_TLS_REJECT_UNAUTHORIZED=`0`;try{return await globalThis.fetch(e,t)}finally{n===void 0?delete process.env.NODE_TLS_REJECT_UNAUTHORIZED:process.env.NODE_TLS_REJECT_UNAUTHORIZED=n}}:globalThis.fetch,u=ML(o),d=u.url;u.host&&(i.Host=u.host);try{let r=_O(e.options.methodOpt),a=HP(),s={redirect:e.options.location?`follow`:`manual`};e.options.jsonBody?(s.body=e.options.jsonBody,i[`Content-Type`]??=`application/json`,i.Accept??=`application/json`):e.options.data&&(s.body=e.options.data),e.options.method?s.method=e.options.method.toUpperCase():s.body&&(s.method=`POST`),Object.keys(i).length>0&&(s.headers=i);let c={include:e.options.include??!1,verbose:e.options.verbose,silent:e.options.silent};e.options.verbose>=2&&dO(o,s,t);let f=await NL(l,o,s,e.options.location??!1),p=f.response;if(p.status!==402){if(e.options.session!==`auto`)return e.error({code:`UNSUPPORTED_SESSION`,message:`--session requires a tempo/session payment challenge.`,exitCode:2});if(e.options.fail&&p.status>=400)return e.error({code:`HTTP_ERROR`,message:`HTTP error ${p.status}`,exitCode:22});fO(p,c),await bL(p);return}let m=p.url?new URL(p.url):void 0,h=u.host!==void 0&&m?.hostname===`127.0.0.1`,g=await kL(p,e.options.protocol,{ignoreX402ResponseUrl:h});if(g.length===0)return e.error({code:`UNSUPPORTED_PROTOCOL`,message:e.options.protocol===`auto`?`Server did not offer a payment challenge.`:`Server did not offer an ${e.options.protocol} payment challenge.`,exitCode:2});let _=OL(g,e.options.currency);if(e.options.currency&&_.length===0){let t=g.map(DL).filter(e=>typeof e==`string`).join(`, `);return e.error({code:`UNSUPPORTED_CURRENCY`,message:`Server did not offer payment currency ${e.options.currency}.${t?` Offered currencies: ${t}.`:``}`,exitCode:2})}let v=CP(n?.config)?.length?_:await IN(_,{options:{account:e.options.account,network:e.options.network,rpcUrl:e.options.rpcUrl}}),y=xP(v,n?.config);if(!y){let t=v.map(e=>`${e.method}/${e.intent}`).join(`, `);return e.error({code:`UNSUPPORTED_METHOD`,message:`Unsupported payment method. Server offers: ${t}. Add it to mppx.config.ts using defineConfig().`,exitCode:2})}let{challenge:b,plugin:x,method:S}=y,C=wS(b),w=C?void 0:PL(o,u,f.logicalUrl),T=jL(b,p),ee=b.request.currency??``,E=b.request.decimals??6,D,O;x&&(O=await x.setup({challenge:b,options:{account:e.options.account,autoSwap:e.options.autoSwap,network:e.options.network,payWith:e.options.payWith,rpcUrl:e.options.rpcUrl,slippage:e.options.slippage},methodOpts:r}),ee=O.tokenSymbol,E=O.tokenDecimals,D=O.explorerUrl);let te=!e.options.silent&&e.options.confirm,ne=new Set;{fO(p,b.method===`tempo`&&b.intent===`session`&&e.options.include?{...c,include:!1,verbose:Math.max(c.verbose,2)}:c);let n=(()=>{let e=new Set([`id`,`request`]),t=[];for(let[n,r]of Object.entries(b))e.has(n)||r===void 0||t.push([n,hO(n,r)]);return t.sort(([e],[t])=>e.localeCompare(t))})(),r={tokenSymbol:ee,tokenDecimals:E,explorerUrl:D},i=(()=>{let e=new Set([`decimals`,`currency`,`methodDetails`]),t=[];for(let[n,i]of Object.entries(b.request))e.has(n)||i===void 0||t.push([n,pO(n,i,r)]);return t.sort(([e],[t])=>e.localeCompare(t))})(),a=(()=>{let e=b.request.methodDetails;if(!e)return[];let t=[];for(let[n,i]of Object.entries(e))if(i!==void 0){if(n===`memo`&&typeof i==`string`){let e=mO(i);t.push([n,e?`${e}\n${X.dim(i)}`:i])}else t.push([n,pO(n,i,r)])}return t.sort(([e],[t])=>e.localeCompare(t))})(),o=[[`Challenge`,n],[`Request`,i],...a.length?[[`Details`,a]]:[]];for(let[,e]of o)for(let[t]of e)ne.add(t);let s=Math.max(...o.flatMap(([,e])=>e.map(([e])=>e.length))),l=` ${``.padEnd(s)} `;if(e.options.verbose>=1||te){t(`${X.bold(X.yellow(`Payment Required`))}\n`);for(let[e,n]of o){t(`${X.bold(e)}\n`);for(let[e,r]of n){let[n,...i]=r.split(`
|
|
246
246
|
`);t(` ${X.dim(e.padEnd(s))} ${n}\n`);for(let e of i)t(`${l}${e}\n`)}}}if(te&&(t(`
|
|
247
247
|
`),!await xO(`Proceed with ${b.intent}?`,!0))){t(`Aborted.
|
|
248
248
|
`);return}}let re=process.env.MPPX_PRIVATE_KEY?.trim()||!yO(WO(e.options.account));if(SD(b)&&re&&!S){try{await UF({challenge:b,challengeResponse:T,credentialContext:await yP(b,n?.config.extensions,O?.credentialContext),endpoint:f.logicalUrl,fetch:l,fetchInput:w.url,init:{...f.init,redirect:`manual`},info:t,methodOptions:r,options:{account:e.options.account,fail:e.options.fail,include:e.options.include,network:e.options.network,rpcUrl:e.options.rpcUrl,session:e.options.session,silent:e.options.silent,verbose:e.options.verbose},registry:a})}catch(e){return MF(e,`SESSION_REQUEST_FAILED`)}return}if(e.options.session!==`auto`)return e.error({code:`UNSUPPORTED_SESSION`,message:S&&SD(b)?`--session cannot override a configured session method. Configure its channelStore instead.`:`--session requires a tempo/session payment challenge.`,exitCode:2});let A=S&&SD(b)?void 0:await yP(b,n?.config.extensions,O?.credentialContext),j,ie;if(S&&SD(b)){let e=T;j=await dT({methods:[S],polyfill:!1,async onChallenge(e,{createCredential:t}){let r=await yP(e,n?.config.extensions,O?.credentialContext);return DP(b,e),t(r)},fetch:async(t,n)=>{if(e){let t=e;return e=void 0,t}return l(t,n)}}).fetch(w.url,{...f.init,context:A,redirect:`manual`})}else{if(O?.createCredential)ie=await O.createCredential(T,A);else if(O)ie=await dT({methods:O.methods,polyfill:!1,transport:AL(b)}).createCredential(T,A);else if(S)ie=await dT({methods:[S],polyfill:!1,transport:AL(b)}).createCredential(T,A);else throw Error(`unreachable`);let n=C?uC:dS(b),r={...Sw(f.init.headers),[n]:ie},i=C?ML((LL(b)??p.url)||o):w;C&&IL(r,i.host),x?.prepareCredentialRequest?.({challenge:b,credential:ie,headers:r});let a={...f.init,redirect:`manual`,headers:r};e.options.verbose>=2&&dO(i.url,a,t),j=await l(i.url,a)}if(e.options.fail&&j.status>=400)return e.error({code:`HTTP_ERROR`,message:`HTTP error ${j.status}`,exitCode:22});if(j.status===402){let n=await j.text();t(`${X.bold(X.red(`Payment Rejected`))}\n`);try{let e=JSON.parse(n),r=[];for(let[t,n]of Object.entries(e))n!==void 0&&r.push([t,String(n)]);r.sort(([e],[t])=>e.localeCompare(t));let i=Math.max(...r.map(([e])=>e.length));for(let[e,n]of r)t(` ${X.dim(e.padEnd(i))} ${n}\n`)}catch{n&&t(` ${n}\n`)}return e.error({code:`PAYMENT_REJECTED`,message:`Payment rejected`,exitCode:75})}if(fO(j,c),!(ie&&await x?.handleResponse?.({challenge:b,credential:ie,response:j,fetchUrl:d,fetchInit:s,silent:e.options.silent,verbose:e.options.verbose,confirmEnabled:te,confirm:xO,tokenSymbol:ee,tokenDecimals:E,explorerUrl:D,shownKeys:ne}))){let n=j.headers.get(q.paymentReceipt);if(n&&e.options.verbose>=1)try{let e=JSON.parse(k.toString(n));t(`\n${X.bold(X.green(`Payment Receipt`))}\n`);let r=[],i=e.channelId,a=e.reference,o=i&&a&&i===a,s=new Set([`acceptedCumulative`,`spent`]);for(let[t,n]of Object.entries(e)){if(n===void 0||ne.has(t)||t===`reference`&&o)continue;let e=x?.formatReceiptField?.(t,n);e===void 0?s.has(t)&&typeof n==`string`?r.push([t,`${n} ${X.dim(`(${SO(BigInt(n),ee,E)})`)}`]):(t===`reference`||t===`txHash`)&&typeof n==`string`&&D?r.push([t,gO(`${D}/tx/${n}`,n)]):r.push([t,String(n)]):r.push([t,e])}r.sort(([e],[t])=>e.localeCompare(t));let c=Math.max(...r.map(([e])=>e.length));for(let[e,n]of r)t(` ${X.dim(e.padEnd(c))} ${n}\n`);t(`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Current mppx package version. */
|
|
2
|
-
export declare const version = "0.13.
|
|
2
|
+
export declare const version = "0.13.3";
|
|
3
3
|
/** Canonical SDK identifier for this mppx release. */
|
|
4
|
-
export declare const sdkIdentifier = "mppx/0.13.
|
|
4
|
+
export declare const sdkIdentifier = "mppx/0.13.3";
|
|
5
5
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/internal/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/sync-version.ts.
|
|
2
2
|
/** Current mppx package version. */
|
|
3
|
-
export const version = '0.13.
|
|
3
|
+
export const version = '0.13.3';
|
|
4
4
|
/** Canonical SDK identifier for this mppx release. */
|
|
5
5
|
export const sdkIdentifier = `mppx/${version}`;
|
|
6
6
|
//# sourceMappingURL=version.js.map
|
|
@@ -2,6 +2,7 @@ import * as Store from '../../Store.js';
|
|
|
2
2
|
import * as Currencies from '../internal/currencies.js';
|
|
3
3
|
import * as ChannelStore from '../session/server/ChannelStore.js';
|
|
4
4
|
import { charge as sessionCharge_, session as session_, settle as settle_, settleBatch as settleBatch_, } from '../session/server/Session.js';
|
|
5
|
+
import * as Settlement from '../session/server/Settlement.js';
|
|
5
6
|
import * as Ws_ from '../session/server/Ws.js';
|
|
6
7
|
import { charge as charge_ } from './Charge.js';
|
|
7
8
|
import { renew as renewSubscription_, subscription as subscription_ } from './Subscription.js';
|
|
@@ -126,7 +127,8 @@ function sessionOffers(parameters) {
|
|
|
126
127
|
serveWebSocket: (options) => Ws_.serve({
|
|
127
128
|
...options,
|
|
128
129
|
store: ChannelStore.fromStore(store),
|
|
129
|
-
|
|
130
|
+
// Failed settlements are reported by the session handler; the charged request stays served.
|
|
131
|
+
onChargeCommitted: (channel) => settleScheduled(channel).catch(Settlement.ignoreRetryableSettlementFailure),
|
|
130
132
|
}),
|
|
131
133
|
};
|
|
132
134
|
function create(currency) {
|
|
@@ -4,6 +4,6 @@ export * as Ws from '../session/server/Ws.js';
|
|
|
4
4
|
export { charge } from './Charge.js';
|
|
5
5
|
export { tempo } from './Methods.js';
|
|
6
6
|
export { session, settle, settleBatch } from '../session/server/Session.js';
|
|
7
|
-
export type { OnSessionSettlement, SessionSettlementContext, SettlementSchedule, } from '../session/server/Session.js';
|
|
7
|
+
export type { OnSessionSettlement, OnSessionSettlementFailure, SessionSettlementContext, SessionSettlementFailureContext, SettlementSchedule, } from '../session/server/Session.js';
|
|
8
8
|
export { renew as renewSubscription, subscription } from './Subscription.js';
|
|
9
9
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -183,6 +183,8 @@ export type ChannelTransactionOptions = {
|
|
|
183
183
|
feePayerPolicy?: Partial<FeePayer.Policy> | undefined;
|
|
184
184
|
/** Explicit fee token for the transaction. */
|
|
185
185
|
feeToken?: Address | undefined;
|
|
186
|
+
/** Unix time in seconds after which the expiring-nonce transaction cannot be included. */
|
|
187
|
+
validBefore?: number | undefined;
|
|
186
188
|
};
|
|
187
189
|
/** Simulates an unsponsored client-signed session transaction without broadcasting it. */
|
|
188
190
|
export declare function simulateCredentialTransaction(parameters: {
|
|
@@ -326,7 +326,7 @@ async function signTempoTransaction(client, transaction) {
|
|
|
326
326
|
return (await signTransaction(client, transaction));
|
|
327
327
|
}
|
|
328
328
|
async function prepareFeePayerCallTransaction(client, parameters) {
|
|
329
|
-
const { account, data, feeToken, to } = parameters;
|
|
329
|
+
const { account, data, feeToken, to, validBefore } = parameters;
|
|
330
330
|
// viem's stable request type does not expose Tempo fee-payer transaction
|
|
331
331
|
// fields for this call shape. Keep the cast at the boundary.
|
|
332
332
|
return prepareTransactionRequest(client, {
|
|
@@ -335,10 +335,11 @@ async function prepareFeePayerCallTransaction(client, parameters) {
|
|
|
335
335
|
feePayer: true,
|
|
336
336
|
nonceKey: 'expiring',
|
|
337
337
|
...(feeToken ? { feeToken } : {}),
|
|
338
|
+
...(validBefore ? { validBefore } : {}),
|
|
338
339
|
});
|
|
339
340
|
}
|
|
340
341
|
function sendPrecompileContractCall(client, parameters) {
|
|
341
|
-
const { account, data, feePayer, feeToken, to } = parameters;
|
|
342
|
+
const { account, data, feePayer, feeToken, to, validBefore } = parameters;
|
|
342
343
|
// `feeToken` is Tempo-specific and not represented on viem's base
|
|
343
344
|
// transaction request type.
|
|
344
345
|
return sendViemTransaction(client, {
|
|
@@ -349,6 +350,7 @@ function sendPrecompileContractCall(client, parameters) {
|
|
|
349
350
|
nonceKey: 'expiring',
|
|
350
351
|
...(feePayer ? { feePayer } : {}),
|
|
351
352
|
...(feeToken ? { feeToken } : {}),
|
|
353
|
+
...(validBefore ? { validBefore } : {}),
|
|
352
354
|
});
|
|
353
355
|
}
|
|
354
356
|
/**
|
|
@@ -674,6 +676,7 @@ async function sendPrecompileTransaction(client, to, data, label, options) {
|
|
|
674
676
|
feePayer: true,
|
|
675
677
|
feeToken: options.feeToken,
|
|
676
678
|
to,
|
|
679
|
+
validBefore: options.validBefore,
|
|
677
680
|
});
|
|
678
681
|
}
|
|
679
682
|
if (feePayer && !selfSponsored) {
|
|
@@ -690,6 +693,7 @@ async function sendPrecompileTransaction(client, to, data, label, options) {
|
|
|
690
693
|
data,
|
|
691
694
|
feeToken,
|
|
692
695
|
to,
|
|
696
|
+
validBefore: options.validBefore,
|
|
693
697
|
});
|
|
694
698
|
assertPrecompileFeePayerPolicy({ prepared, policy: options.feePayerPolicy });
|
|
695
699
|
const serialized = await signTempoTransaction(client, {
|
|
@@ -719,6 +723,7 @@ async function sendPrecompileTransaction(client, to, data, label, options) {
|
|
|
719
723
|
to,
|
|
720
724
|
data,
|
|
721
725
|
feeToken,
|
|
726
|
+
validBefore: options?.validBefore,
|
|
722
727
|
});
|
|
723
728
|
}
|
|
724
729
|
//# sourceMappingURL=Chain.js.map
|
|
@@ -300,6 +300,14 @@ export declare function acceptVoucherStateUpdate(parameters: AcceptVoucherStateU
|
|
|
300
300
|
* close voucher and current on-chain deposit.
|
|
301
301
|
*/
|
|
302
302
|
export declare function resolveCloseCaptureAmount(parameters: ResolveCloseCaptureAmountParameters): bigint;
|
|
303
|
+
/** Seconds a server close transaction stays valid after its pending-close marker is written. */
|
|
304
|
+
export declare const pendingCloseValiditySeconds = 25;
|
|
305
|
+
/** Clears a server pending-close marker whose close transaction expired without closing the channel. */
|
|
306
|
+
export declare function reconcileExpiredPendingClose(parameters: {
|
|
307
|
+
current: State | null;
|
|
308
|
+
now: number;
|
|
309
|
+
state: OnChainChannelState;
|
|
310
|
+
}): State | null;
|
|
303
311
|
/** Marks local channel state as pending close and returns the bounded capture amount. */
|
|
304
312
|
export declare function markPendingClose(parameters: MarkPendingCloseParameters): PendingCloseUpdate;
|
|
305
313
|
/** Finalizes local channel state after a successful close transaction. */
|
|
@@ -142,6 +142,23 @@ export function resolveCloseCaptureAmount(parameters) {
|
|
|
142
142
|
}
|
|
143
143
|
return captureAmount;
|
|
144
144
|
}
|
|
145
|
+
/** Seconds a server close transaction stays valid after its pending-close marker is written. */
|
|
146
|
+
export const pendingCloseValiditySeconds = 25;
|
|
147
|
+
/** Extra seconds allowed for clock differences between the server and the chain. */
|
|
148
|
+
const pendingCloseClockSkewSeconds = 30;
|
|
149
|
+
/** Clears a server pending-close marker whose close transaction expired without closing the channel. */
|
|
150
|
+
export function reconcileExpiredPendingClose(parameters) {
|
|
151
|
+
const { current, now, state } = parameters;
|
|
152
|
+
if (!current || current.finalized || current.closeRequestedAt === 0n)
|
|
153
|
+
return current;
|
|
154
|
+
// An open channel with no on-chain close request proves the expired close never executed.
|
|
155
|
+
if (BigInt(state.closeRequestedAt) !== 0n || state.deposit === 0n)
|
|
156
|
+
return current;
|
|
157
|
+
const expiresAt = current.closeRequestedAt + BigInt(pendingCloseValiditySeconds);
|
|
158
|
+
if (BigInt(now) <= expiresAt + BigInt(pendingCloseClockSkewSeconds))
|
|
159
|
+
return current;
|
|
160
|
+
return { ...current, closeRequestedAt: 0n };
|
|
161
|
+
}
|
|
145
162
|
/** Marks local channel state as pending close and returns the bounded capture amount. */
|
|
146
163
|
export function markPendingClose(parameters) {
|
|
147
164
|
const { closeRequestedAt, cumulativeAmount, current, onChainSettled, onChainDeposit } = parameters;
|
|
@@ -5,7 +5,7 @@ import * as Chain from '../precompile/Chain.js';
|
|
|
5
5
|
import * as Channel from '../precompile/Channel.js';
|
|
6
6
|
import { type SessionCredentialPayload, type SessionReceipt } from '../precompile/Protocol.js';
|
|
7
7
|
import * as ChannelStore from './ChannelStore.js';
|
|
8
|
-
import { type OnSessionSettlement } from './Settlement.js';
|
|
8
|
+
import { type OnSessionSettlement, type OnSessionSettlementFailure } from './Settlement.js';
|
|
9
9
|
/** Returns the effective voucher signer for a TIP-1034 descriptor. */
|
|
10
10
|
export declare function authorizedSigner(descriptor: Channel.ChannelDescriptor): Address;
|
|
11
11
|
/** Asserts that a credential payload includes a TIP-1034 descriptor. */
|
|
@@ -77,6 +77,8 @@ export type BroadcastCredentialPayloadParameters = {
|
|
|
77
77
|
minVoucherDelta: bigint;
|
|
78
78
|
/** Callback invoked after an on-chain settlement or close transaction is confirmed. */
|
|
79
79
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
80
|
+
/** Callback invoked when a scheduled settlement or close transaction fails. */
|
|
81
|
+
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
80
82
|
/** Discriminated session credential payload to verify. */
|
|
81
83
|
payload: SessionCredentialPayload;
|
|
82
84
|
/** Whether an open or voucher credential must add new funds for this request. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAddress, isAddressEqual, zeroAddress, } from 'viem';
|
|
1
|
+
import { createClient, custom, isAddress, isAddressEqual, zeroAddress, } from 'viem';
|
|
2
2
|
import { AmountExceedsDepositError, ChannelClosedError, ChannelNotFoundError, DeltaTooSmallError, InsufficientBalanceError, InvalidSignatureError, VerificationFailedError, } from '../../../Errors.js';
|
|
3
3
|
import * as Chain from '../precompile/Chain.js';
|
|
4
4
|
import { readChannelClosedReceiptFields } from '../precompile/Chain.js';
|
|
@@ -7,7 +7,7 @@ import { createSessionReceipt, uint96, } from '../precompile/Protocol.js';
|
|
|
7
7
|
import * as Voucher from '../precompile/Voucher.js';
|
|
8
8
|
import * as ChannelStore from './ChannelStore.js';
|
|
9
9
|
import { getChallengePaymentFields } from './RequestState.js';
|
|
10
|
-
import { assertSettlementSender, getClientAccount, maybeSettleScheduled, } from './Settlement.js';
|
|
10
|
+
import { assertSettlementSender, getClientAccount, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
11
11
|
/** Returns the effective voucher signer for a TIP-1034 descriptor. */
|
|
12
12
|
export function authorizedSigner(descriptor) {
|
|
13
13
|
return isAddressEqual(descriptor.authorizedSigner, zeroAddress)
|
|
@@ -622,6 +622,13 @@ async function handleVoucherCredential(parameters) {
|
|
|
622
622
|
forceRefresh: true,
|
|
623
623
|
lastOnChainVerified,
|
|
624
624
|
});
|
|
625
|
+
const reconciled = channel.closeRequestedAt === 0n
|
|
626
|
+
? channel
|
|
627
|
+
: ((await store.updateChannel(channelId, (current) => ChannelStore.reconcileExpiredPendingClose({
|
|
628
|
+
current,
|
|
629
|
+
now: Math.floor(Date.now() / 1000),
|
|
630
|
+
state: channelState,
|
|
631
|
+
}))) ?? channel);
|
|
625
632
|
if (channelState.closeRequestedAt !== 0) {
|
|
626
633
|
const closing = await store.updateChannel(channelId, (current) => current
|
|
627
634
|
? {
|
|
@@ -643,6 +650,7 @@ async function handleVoucherCredential(parameters) {
|
|
|
643
650
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
644
651
|
feeToken: parameters.feeToken,
|
|
645
652
|
onSessionSettlement: parameters.onSessionSettlement,
|
|
653
|
+
onSessionSettlementFailure: parameters.onSessionSettlementFailure,
|
|
646
654
|
schedule: {},
|
|
647
655
|
store,
|
|
648
656
|
});
|
|
@@ -653,12 +661,36 @@ async function handleVoucherCredential(parameters) {
|
|
|
653
661
|
minVoucherDelta,
|
|
654
662
|
requireAdvance: parameters.requireVoucherAdvance,
|
|
655
663
|
challenge,
|
|
656
|
-
channel,
|
|
664
|
+
channel: reconciled,
|
|
657
665
|
voucher,
|
|
658
666
|
channelState,
|
|
659
667
|
methodDetails: { chainId, escrowContract: escrow },
|
|
660
668
|
});
|
|
661
669
|
}
|
|
670
|
+
const sendMethods = new Set([
|
|
671
|
+
'eth_sendRawTransaction',
|
|
672
|
+
'eth_sendRawTransactionSync',
|
|
673
|
+
'eth_sendTransaction',
|
|
674
|
+
]);
|
|
675
|
+
/** Wraps a client so a failed close can tell whether its transaction was handed to the node. */
|
|
676
|
+
function trackSendAttempts(client) {
|
|
677
|
+
const tracked = { attempted: false };
|
|
678
|
+
const trackedClient = createClient({
|
|
679
|
+
account: client.account,
|
|
680
|
+
chain: client.chain,
|
|
681
|
+
pollingInterval: client.pollingInterval,
|
|
682
|
+
transport: custom({
|
|
683
|
+
request(parameters) {
|
|
684
|
+
if (sendMethods.has(parameters.method))
|
|
685
|
+
tracked.attempted = true;
|
|
686
|
+
return client.request(parameters);
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
// The wrapped client already owns retries.
|
|
690
|
+
{ retryCount: 0 }),
|
|
691
|
+
});
|
|
692
|
+
return { client: trackedClient, tracked };
|
|
693
|
+
}
|
|
662
694
|
async function handleCloseCredential(parameters) {
|
|
663
695
|
const { store, client, challenge, payload, chainId, escrow } = parameters;
|
|
664
696
|
const request = getChallengePaymentFields(challenge);
|
|
@@ -698,14 +730,14 @@ async function handleCloseCredential(parameters) {
|
|
|
698
730
|
let captureAmount = uint96(channel.spent > state.settled ? channel.spent : state.settled);
|
|
699
731
|
if (captureAmount > state.deposit)
|
|
700
732
|
throw new AmountExceedsDepositError({ reason: 'close capture amount exceeds on-chain deposit' });
|
|
701
|
-
const
|
|
702
|
-
const
|
|
733
|
+
const now = Math.floor(Date.now() / 1000) || 1;
|
|
734
|
+
const pendingCloseStartedAt = BigInt(now);
|
|
703
735
|
let pendingCloseMarked = false;
|
|
704
736
|
await store.updateChannel(channelId, (current) => {
|
|
705
737
|
const next = ChannelStore.markPendingClose({
|
|
706
738
|
closeRequestedAt: pendingCloseStartedAt,
|
|
707
739
|
cumulativeAmount,
|
|
708
|
-
current,
|
|
740
|
+
current: ChannelStore.reconcileExpiredPendingClose({ current, now, state }),
|
|
709
741
|
onChainDeposit: state.deposit,
|
|
710
742
|
onChainSettled: state.settled,
|
|
711
743
|
});
|
|
@@ -716,8 +748,11 @@ async function handleCloseCredential(parameters) {
|
|
|
716
748
|
return next.state;
|
|
717
749
|
});
|
|
718
750
|
const account = parameters.account ?? getClientAccount(client);
|
|
719
|
-
|
|
751
|
+
// Pin the close transaction's expiry to the marker so an expired marker proves it can no longer land.
|
|
752
|
+
const validBefore = now + ChannelStore.pendingCloseValiditySeconds;
|
|
753
|
+
const send = trackSendAttempts(client);
|
|
720
754
|
let receipt;
|
|
755
|
+
let txHash;
|
|
721
756
|
try {
|
|
722
757
|
assertSettlementSender({
|
|
723
758
|
operation: 'close',
|
|
@@ -726,21 +761,31 @@ async function handleCloseCredential(parameters) {
|
|
|
726
761
|
payee: channel.payee,
|
|
727
762
|
sender: account?.address,
|
|
728
763
|
});
|
|
729
|
-
txHash = await Chain.closeOnChain(client, channel.descriptor, cumulativeAmount, captureAmount, payload.signature, escrow, account
|
|
764
|
+
txHash = await Chain.closeOnChain(send.client, channel.descriptor, cumulativeAmount, captureAmount, payload.signature, escrow, account
|
|
730
765
|
? {
|
|
731
766
|
account,
|
|
732
767
|
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
733
768
|
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
734
769
|
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
735
770
|
candidateFeeTokens: [channel.token],
|
|
771
|
+
validBefore,
|
|
736
772
|
}
|
|
737
|
-
:
|
|
773
|
+
: { validBefore });
|
|
738
774
|
receipt = await Chain.waitForSuccessfulReceipt(client, txHash);
|
|
739
775
|
}
|
|
740
776
|
catch (error) {
|
|
741
|
-
|
|
777
|
+
await reportSessionSettlementFailure(parameters.onSessionSettlementFailure, {
|
|
778
|
+
chainId,
|
|
779
|
+
channelId,
|
|
780
|
+
error,
|
|
781
|
+
trigger: 'close',
|
|
782
|
+
});
|
|
783
|
+
// A close whose send RPC was attempted may still land, so it keeps the marker until it expires and
|
|
784
|
+
// a later credential reconciles it. Verification errors prove a rejection or revert.
|
|
785
|
+
const closeFailed = !send.tracked.attempted || error instanceof VerificationFailedError;
|
|
786
|
+
if (pendingCloseMarked && closeFailed) {
|
|
742
787
|
await store.updateChannel(channelId, (current) => current && current.closeRequestedAt === pendingCloseStartedAt
|
|
743
|
-
? { ...current, closeRequestedAt:
|
|
788
|
+
? { ...current, closeRequestedAt: 0n }
|
|
744
789
|
: current);
|
|
745
790
|
}
|
|
746
791
|
throw error;
|
|
@@ -19,12 +19,12 @@ import { deserializeSnapshot as deserializeSessionSnapshot, serializeSnapshot as
|
|
|
19
19
|
import * as ChannelStore from './ChannelStore.js';
|
|
20
20
|
import { type ResolveSessionChannelId } from './RequestState.js';
|
|
21
21
|
import { type SettleChargedSessionChannel } from './Settlement.js';
|
|
22
|
-
import { type OnSessionSettlement, type SettlementSchedule } from './Settlement.js';
|
|
22
|
+
import { type OnSessionSettlement, type OnSessionSettlementFailure, type SettlementSchedule } from './Settlement.js';
|
|
23
23
|
import * as Ws from './Ws.js';
|
|
24
24
|
/** Server-side automatic settlement schedule. */
|
|
25
25
|
export type { SettlementSchedule } from './Settlement.js';
|
|
26
26
|
/** Server-side settlement event hook types. */
|
|
27
|
-
export type { OnSessionSettlement, SessionSettlementContext } from './Settlement.js';
|
|
27
|
+
export type { OnSessionSettlement, OnSessionSettlementFailure, SessionSettlementContext, SessionSettlementFailureContext, } from './Settlement.js';
|
|
28
28
|
/** Server-side hook types for request-identity channel bootstrap. */
|
|
29
29
|
export type { ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, } from './RequestState.js';
|
|
30
30
|
export { settle, settleBatch } from './Settlement.js';
|
|
@@ -175,6 +175,8 @@ export declare namespace session {
|
|
|
175
175
|
escrowContract?: Address | undefined;
|
|
176
176
|
/** Callback invoked after any on-chain settlement or close transaction is confirmed. */
|
|
177
177
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
178
|
+
/** Callback invoked when a scheduled settlement or close transaction fails. Observer errors are ignored. */
|
|
179
|
+
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
178
180
|
/** Server-owned automatic settlement cadence. Clients do not receive or control this schedule. */
|
|
179
181
|
settlementSchedule?: SettlementSchedule | undefined;
|
|
180
182
|
/** Optional fee token for management and server-driven settle/close transactions. */
|
|
@@ -27,7 +27,7 @@ import { requireSessionCredentialPayload } from './CredentialVerification.js';
|
|
|
27
27
|
import { resolveCredentialVerificationContext, resolveSessionChannelId, resolveSessionSnapshot, resolveSessionPaymentRequest, } from './RequestState.js';
|
|
28
28
|
import { respondToSessionCredential } from './RequestState.js';
|
|
29
29
|
import { applyVerifiedHttpAccounting, chargeSessionChannel, shouldApplyVerifiedHttpAccounting, } from './Settlement.js';
|
|
30
|
-
import { isSettlementDue, maybeSettleScheduled } from './Settlement.js';
|
|
30
|
+
import { ignoreRetryableSettlementFailure, isSettlementDue, maybeSettleScheduled, reportSessionSettlementFailure, } from './Settlement.js';
|
|
31
31
|
import { resolveSettlementSchedule, } from './Settlement.js';
|
|
32
32
|
import * as Ws from './Ws.js';
|
|
33
33
|
export { settle, settleBatch } from './Settlement.js';
|
|
@@ -196,6 +196,7 @@ export function session(p) {
|
|
|
196
196
|
const { amount, channelStateTtl = 5_000, currency = defaults.resolveCurrency(parameters), decimals = defaults.decimals, operator, store: rawStore = Store.memory(), suggestedDeposit, unitType, } = parameters;
|
|
197
197
|
const settlementSchedule = resolveSettlementSchedule(parameters.settlementSchedule, decimals);
|
|
198
198
|
const onSessionSettlement = parameters.onSessionSettlement;
|
|
199
|
+
const onSessionSettlementFailure = parameters.onSessionSettlementFailure;
|
|
199
200
|
const store = ChannelStore.fromStore(rawStore);
|
|
200
201
|
const lastOnChainVerified = new Map();
|
|
201
202
|
const { account, feePayer, remoteFeePayer, recipient } = Account.resolve(parameters);
|
|
@@ -209,21 +210,34 @@ export function session(p) {
|
|
|
209
210
|
const settleScheduled = async (channel) => {
|
|
210
211
|
if (!isSettlementDue(channel, settlementSchedule))
|
|
211
212
|
return undefined;
|
|
213
|
+
const client = await (async () => getClient({ chainId: channel.chainId }))().catch(async (error) => {
|
|
214
|
+
await reportSessionSettlementFailure(onSessionSettlementFailure, {
|
|
215
|
+
chainId: channel.chainId,
|
|
216
|
+
channelId: channel.channelId,
|
|
217
|
+
error,
|
|
218
|
+
trigger: 'scheduled',
|
|
219
|
+
});
|
|
220
|
+
throw error;
|
|
221
|
+
});
|
|
212
222
|
return maybeSettleScheduled({
|
|
213
223
|
account,
|
|
214
224
|
channel,
|
|
215
|
-
client
|
|
225
|
+
client,
|
|
216
226
|
...(configuredFeePayer ? { feePayer: configuredFeePayer } : {}),
|
|
217
227
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
218
228
|
feeToken: parameters.feeToken,
|
|
219
229
|
onSessionSettlement,
|
|
230
|
+
onSessionSettlementFailure,
|
|
220
231
|
schedule: settlementSchedule,
|
|
221
232
|
store,
|
|
222
233
|
});
|
|
223
234
|
};
|
|
235
|
+
// A failed scheduled settlement is reported and retried by the next one; the charged request
|
|
236
|
+
// stays served because the payer's voucher already covers the charge.
|
|
237
|
+
const settleCharged = (channel) => settleScheduled(channel).catch(ignoreRetryableSettlementFailure);
|
|
224
238
|
const serveWebSocket = (options) => Ws.serve({
|
|
225
239
|
...options,
|
|
226
|
-
onChargeCommitted:
|
|
240
|
+
onChargeCommitted: settleCharged,
|
|
227
241
|
store,
|
|
228
242
|
});
|
|
229
243
|
const bootstrapCharge = ChargeServer.charge({
|
|
@@ -238,7 +252,7 @@ export function session(p) {
|
|
|
238
252
|
});
|
|
239
253
|
const transport = parameters.sse
|
|
240
254
|
? Transport.sse({
|
|
241
|
-
settleCharged
|
|
255
|
+
settleCharged,
|
|
242
256
|
store,
|
|
243
257
|
...(typeof parameters.sse === 'object' ? parameters.sse : undefined),
|
|
244
258
|
})
|
|
@@ -305,6 +319,7 @@ export function session(p) {
|
|
|
305
319
|
lastOnChainVerified,
|
|
306
320
|
minVoucherDelta: context.minVoucherDelta,
|
|
307
321
|
onSessionSettlement,
|
|
322
|
+
onSessionSettlementFailure,
|
|
308
323
|
payload,
|
|
309
324
|
requireVoucherAdvance: shouldApplyVerifiedHttpAccounting({
|
|
310
325
|
capturedRequest: envelope?.capturedRequest,
|
|
@@ -328,10 +343,11 @@ export function session(p) {
|
|
|
328
343
|
feePayerPolicy: parameters.feePayerPolicy,
|
|
329
344
|
feeToken: parameters.feeToken,
|
|
330
345
|
onSessionSettlement,
|
|
346
|
+
onSessionSettlementFailure,
|
|
331
347
|
schedule: settlementSchedule,
|
|
332
348
|
store,
|
|
333
349
|
channel,
|
|
334
|
-
}),
|
|
350
|
+
}).catch(ignoreRetryableSettlementFailure),
|
|
335
351
|
});
|
|
336
352
|
};
|
|
337
353
|
const method = Method.toServer(Methods.session, {
|
|
@@ -86,6 +86,19 @@ export type SessionSettlementContext = Readonly<{
|
|
|
86
86
|
}>;
|
|
87
87
|
/** Callback invoked after an on-chain settlement or close transaction is confirmed. */
|
|
88
88
|
export type OnSessionSettlement = (context: SessionSettlementContext) => MaybePromise<void>;
|
|
89
|
+
/** Context emitted when a scheduled settlement or close transaction fails. */
|
|
90
|
+
export type SessionSettlementFailureContext = Readonly<{
|
|
91
|
+
/** Chain ID of the channel. */
|
|
92
|
+
chainId: number;
|
|
93
|
+
/** Channel ID whose settlement or close failed. */
|
|
94
|
+
channelId: Hex;
|
|
95
|
+
/** Error thrown by the settlement or close. */
|
|
96
|
+
error: unknown;
|
|
97
|
+
/** `close` for a failed close transaction; `scheduled` for a failed scheduled settlement. */
|
|
98
|
+
trigger: 'close' | 'scheduled';
|
|
99
|
+
}>;
|
|
100
|
+
/** Callback invoked when a scheduled settlement or close transaction fails. */
|
|
101
|
+
export type OnSessionSettlementFailure = (context: SessionSettlementFailureContext) => MaybePromise<void>;
|
|
89
102
|
/** Inputs used to mark a channel after automatic scheduled settlement succeeds. */
|
|
90
103
|
export type MarkSettlementCompleteParameters = {
|
|
91
104
|
channelId: ChannelStore.State['channelId'];
|
|
@@ -105,7 +118,7 @@ export declare function resolveSettlementSchedule(schedule: SettlementSchedule |
|
|
|
105
118
|
export declare function resolveSettlementProgress(channel: ChannelStore.State): SettlementProgress | undefined;
|
|
106
119
|
/** Returns whether the precompile channel has crossed any configured settlement threshold. */
|
|
107
120
|
export declare function isSettlementDue(channel: ChannelStore.State, schedule: ResolvedSettlementSchedule | undefined): boolean;
|
|
108
|
-
/**
|
|
121
|
+
/** Releases a completed scheduled settlement lease; {@link settle} records the settled counters. */
|
|
109
122
|
export declare function markSettlementComplete(parameters: MarkSettlementCompleteParameters): Promise<void>;
|
|
110
123
|
/** Atomically claims one due scheduled settlement across server workers. */
|
|
111
124
|
export declare function claimScheduledSettlement(parameters: {
|
|
@@ -208,6 +221,8 @@ export type MaybeSettleScheduledParameters = {
|
|
|
208
221
|
feeToken?: Address | undefined;
|
|
209
222
|
/** Callback invoked after the scheduled settlement transaction is confirmed. */
|
|
210
223
|
onSessionSettlement?: OnSessionSettlement | undefined;
|
|
224
|
+
/** Callback invoked when the scheduled settlement fails. */
|
|
225
|
+
onSessionSettlementFailure?: OnSessionSettlementFailure | undefined;
|
|
211
226
|
/** Resolved server-owned settlement cadence. */
|
|
212
227
|
schedule: ResolvedSettlementSchedule | undefined;
|
|
213
228
|
/** Server-side channel store. */
|
|
@@ -225,6 +240,23 @@ export declare function assertSettlementSender(parameters: SettlementSenderParam
|
|
|
225
240
|
export declare function maybeSettleScheduled(parameters: MaybeSettleScheduledParameters): Promise<Hex | undefined>;
|
|
226
241
|
/** Settles the highest accepted voucher for a precompile-backed session channel. */
|
|
227
242
|
export declare function settle(store_: SessionStoreInput, client: Chain.TransactionClient, channelId_: Hex, options?: SettlementTransactionOptions): Promise<Hex>;
|
|
243
|
+
/** Raised when a settlement transaction confirmed but the channel store could not record it. */
|
|
244
|
+
export declare class SettlementCheckpointError extends Error {
|
|
245
|
+
readonly name = "SettlementCheckpointError";
|
|
246
|
+
/** Hash of the confirmed settlement transaction. */
|
|
247
|
+
readonly txHash: Hex;
|
|
248
|
+
constructor(options: {
|
|
249
|
+
cause: unknown;
|
|
250
|
+
txHash: Hex;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
228
253
|
/** Settles multiple precompile-backed session channels with the same validation as {@link settle}. */
|
|
229
254
|
export declare function settleBatch(store: SessionStoreInput, client: Chain.TransactionClient, channelIds: readonly Hex[], options?: SettlementTransactionOptions): Promise<Hex[]>;
|
|
255
|
+
/**
|
|
256
|
+
* @internal Keeps a charged request served when its scheduled settlement hit a transport or RPC
|
|
257
|
+
* failure; the next settlement retries it. Reverts and configuration errors fail the request.
|
|
258
|
+
*/
|
|
259
|
+
export declare function ignoreRetryableSettlementFailure(error: unknown): undefined;
|
|
260
|
+
/** @internal Reports a settlement failure without letting observer errors replace it. */
|
|
261
|
+
export declare function reportSessionSettlementFailure(onSessionSettlementFailure: OnSessionSettlementFailure | undefined, context: SessionSettlementFailureContext): Promise<void>;
|
|
230
262
|
//# sourceMappingURL=Settlement.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAddress, isAddressEqual, parseUnits, zeroAddress, } from 'viem';
|
|
1
|
+
import { BaseError as viem_BaseError, HttpRequestError, isAddress, isAddressEqual, LimitExceededRpcError, parseUnits, ResourceUnavailableRpcError, SocketClosedError, TimeoutError, WebSocketRequestError, zeroAddress, } from 'viem';
|
|
2
2
|
import { BadRequestError, ChannelClosedError, ChannelNotFoundError, InsufficientBalanceError, VerificationFailedError, } from '../../../Errors.js';
|
|
3
3
|
import { isSessionContentRequest } from '../../server/internal/request-body.js';
|
|
4
4
|
import * as Chain from '../precompile/Chain.js';
|
|
@@ -105,7 +105,7 @@ export function isSettlementDue(channel, schedule) {
|
|
|
105
105
|
return true;
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
|
-
/**
|
|
108
|
+
/** Releases a completed scheduled settlement lease; {@link settle} records the settled counters. */
|
|
109
109
|
export async function markSettlementComplete(parameters) {
|
|
110
110
|
const { channelId, leaseOwner, store, settledAt = new Date().toISOString() } = parameters;
|
|
111
111
|
await store.updateChannel(channelId, (current) => {
|
|
@@ -114,12 +114,7 @@ export async function markSettlementComplete(parameters) {
|
|
|
114
114
|
if (current.scheduledSettlementLease?.owner !== leaseOwner)
|
|
115
115
|
return current;
|
|
116
116
|
const { scheduledSettlementLease: _, ...channel } = current;
|
|
117
|
-
return {
|
|
118
|
-
...channel,
|
|
119
|
-
lastSettlementAt: settledAt,
|
|
120
|
-
lastSettlementSpent: current.spent,
|
|
121
|
-
lastSettlementUnits: current.units,
|
|
122
|
-
};
|
|
117
|
+
return { ...channel, lastSettlementAt: settledAt };
|
|
123
118
|
});
|
|
124
119
|
}
|
|
125
120
|
/** Atomically claims one due scheduled settlement across server workers. */
|
|
@@ -244,10 +239,19 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
244
239
|
const { channel, schedule, store } = parameters;
|
|
245
240
|
if (!schedule || !isSettlementDue(channel, schedule))
|
|
246
241
|
return undefined;
|
|
242
|
+
const report = (error) => reportSessionSettlementFailure(parameters.onSessionSettlementFailure, {
|
|
243
|
+
chainId: channel.chainId,
|
|
244
|
+
channelId: channel.channelId,
|
|
245
|
+
error,
|
|
246
|
+
trigger: 'scheduled',
|
|
247
|
+
});
|
|
247
248
|
const leaseOwner = await claimScheduledSettlement({
|
|
248
249
|
channelId: channel.channelId,
|
|
249
250
|
schedule,
|
|
250
251
|
store,
|
|
252
|
+
}).catch(async (error) => {
|
|
253
|
+
await report(error);
|
|
254
|
+
throw error;
|
|
251
255
|
});
|
|
252
256
|
if (!leaseOwner)
|
|
253
257
|
return undefined;
|
|
@@ -258,26 +262,28 @@ export async function maybeSettleScheduled(parameters) {
|
|
|
258
262
|
store,
|
|
259
263
|
}).catch(() => undefined);
|
|
260
264
|
}, scheduledSettlementLeaseMs / 2);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
265
|
+
const release = () => releaseScheduledSettlement({ channelId: channel.channelId, leaseOwner, store }).catch(() => undefined);
|
|
266
|
+
const txHash = await settle(store, parameters.client, channel.channelId, {
|
|
267
|
+
account: parameters.account,
|
|
268
|
+
...(parameters.feePayer ? { feePayer: parameters.feePayer } : {}),
|
|
269
|
+
...(parameters.feePayerPolicy ? { feePayerPolicy: parameters.feePayerPolicy } : {}),
|
|
270
|
+
...(parameters.feeToken ? { feeToken: parameters.feeToken } : {}),
|
|
271
|
+
onSessionSettlement: parameters.onSessionSettlement
|
|
272
|
+
? (ctx) => parameters.onSessionSettlement({ ...ctx, trigger: 'scheduled' })
|
|
273
|
+
: undefined,
|
|
274
|
+
})
|
|
275
|
+
.catch(async (error) => {
|
|
276
|
+
await release();
|
|
277
|
+
// The transaction confirmed and collected the charge; only the local record failed.
|
|
278
|
+
if (error instanceof SettlementCheckpointError)
|
|
279
|
+
return error.txHash;
|
|
280
|
+
await report(error);
|
|
276
281
|
throw error;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
282
|
+
})
|
|
283
|
+
.finally(() => clearInterval(renewal));
|
|
284
|
+
// Bookkeeping after a confirmed settlement must not fail the charged request.
|
|
285
|
+
await markSettlementComplete({ channelId: channel.channelId, leaseOwner, store }).catch(release);
|
|
286
|
+
return txHash;
|
|
281
287
|
}
|
|
282
288
|
/** Settles the highest accepted voucher for a precompile-backed session channel. */
|
|
283
289
|
export async function settle(store_, client, channelId_, options) {
|
|
@@ -319,15 +325,21 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
319
325
|
throw new VerificationFailedError({
|
|
320
326
|
reason: 'on-chain channel state does not match settle receipt',
|
|
321
327
|
});
|
|
322
|
-
|
|
328
|
+
let checkpointError;
|
|
329
|
+
await store
|
|
330
|
+
.updateChannel(channelId, (current) => current
|
|
323
331
|
? {
|
|
324
332
|
...current,
|
|
325
333
|
settledOnChain: newSettled > current.settledOnChain ? newSettled : current.settledOnChain,
|
|
326
334
|
lastSettlementAt: new Date().toISOString(),
|
|
327
|
-
|
|
328
|
-
|
|
335
|
+
// Charges accepted after the voucher was read are not covered by this transaction.
|
|
336
|
+
lastSettlementSpent: ChannelStore.keepGreater(current.lastSettlementSpent ?? 0n, channel.spent),
|
|
337
|
+
lastSettlementUnits: Math.max(current.lastSettlementUnits ?? 0, channel.units),
|
|
329
338
|
}
|
|
330
|
-
: current)
|
|
339
|
+
: current)
|
|
340
|
+
.catch((cause) => {
|
|
341
|
+
checkpointError = { cause };
|
|
342
|
+
});
|
|
331
343
|
if (options?.onSessionSettlement) {
|
|
332
344
|
await emitSessionSettlement(options.onSessionSettlement, {
|
|
333
345
|
txHash,
|
|
@@ -337,8 +349,20 @@ export async function settle(store_, client, channelId_, options) {
|
|
|
337
349
|
delta: newSettled - channel.settledOnChain,
|
|
338
350
|
});
|
|
339
351
|
}
|
|
352
|
+
if (checkpointError)
|
|
353
|
+
throw new SettlementCheckpointError({ cause: checkpointError.cause, txHash });
|
|
340
354
|
return txHash;
|
|
341
355
|
}
|
|
356
|
+
/** Raised when a settlement transaction confirmed but the channel store could not record it. */
|
|
357
|
+
export class SettlementCheckpointError extends Error {
|
|
358
|
+
name = 'SettlementCheckpointError';
|
|
359
|
+
/** Hash of the confirmed settlement transaction. */
|
|
360
|
+
txHash;
|
|
361
|
+
constructor(options) {
|
|
362
|
+
super(`Settlement ${options.txHash} confirmed but was not recorded.`, { cause: options.cause });
|
|
363
|
+
this.txHash = options.txHash;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
342
366
|
/** Settles multiple precompile-backed session channels with the same validation as {@link settle}. */
|
|
343
367
|
export async function settleBatch(store, client, channelIds, options) {
|
|
344
368
|
const hashes = [];
|
|
@@ -346,6 +370,36 @@ export async function settleBatch(store, client, channelIds, options) {
|
|
|
346
370
|
hashes.push(await settle(store, client, channelId, options));
|
|
347
371
|
return hashes;
|
|
348
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* @internal Keeps a charged request served when its scheduled settlement hit a transport or RPC
|
|
375
|
+
* failure; the next settlement retries it. Reverts and configuration errors fail the request.
|
|
376
|
+
*/
|
|
377
|
+
export function ignoreRetryableSettlementFailure(error) {
|
|
378
|
+
if (isRetryableSettlementFailure(error))
|
|
379
|
+
return undefined;
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
function isRetryableSettlementFailure(error) {
|
|
383
|
+
if (!(error instanceof viem_BaseError))
|
|
384
|
+
return false;
|
|
385
|
+
// Only unavailable or overloaded upstreams clear on retry; node-rejected transactions repeat.
|
|
386
|
+
return Boolean(error.walk((cause) => (cause instanceof HttpRequestError &&
|
|
387
|
+
(cause.status === undefined || cause.status === 429 || cause.status >= 500)) ||
|
|
388
|
+
cause instanceof LimitExceededRpcError ||
|
|
389
|
+
cause instanceof ResourceUnavailableRpcError ||
|
|
390
|
+
cause instanceof SocketClosedError ||
|
|
391
|
+
cause instanceof TimeoutError ||
|
|
392
|
+
cause instanceof WebSocketRequestError));
|
|
393
|
+
}
|
|
394
|
+
/** @internal Reports a settlement failure without letting observer errors replace it. */
|
|
395
|
+
export async function reportSessionSettlementFailure(onSessionSettlementFailure, context) {
|
|
396
|
+
try {
|
|
397
|
+
await onSessionSettlementFailure?.(Object.freeze(context));
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Errors are isolated: observers cannot replace the settlement failure.
|
|
401
|
+
}
|
|
402
|
+
}
|
|
349
403
|
async function emitSessionSettlement(onSessionSettlement, context) {
|
|
350
404
|
try {
|
|
351
405
|
await onSessionSettlement(Object.freeze(context));
|
|
@@ -2,5 +2,5 @@ export { charge, session, settle, settleBatch } from './Session.js';
|
|
|
2
2
|
/** SSE helpers and types for Tempo session streams. */
|
|
3
3
|
export * as Sse from './Sse.js';
|
|
4
4
|
/** Server-side automatic settlement schedule. */
|
|
5
|
-
export type { OnSessionSettlement, ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, SessionSettlementContext, SettlementSchedule, } from './Session.js';
|
|
5
|
+
export type { OnSessionSettlement, OnSessionSettlementFailure, ResolveSessionChannelId, ResolveSessionChannelIdParameters, SessionChannelIdRequest, SessionSettlementContext, SessionSettlementFailureContext, SettlementSchedule, } from './Session.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|