@vinaystwt/xmpp-http-interceptor 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/config/src/index.d.ts +49 -0
- package/dist/config/src/index.js +117 -0
- package/dist/contract-runtime/src/index.d.ts +44 -0
- package/dist/contract-runtime/src/index.js +364 -0
- package/dist/http-interceptor/src/agents.d.ts +5 -0
- package/dist/http-interceptor/src/agents.js +88 -0
- package/dist/http-interceptor/src/agents.test.d.ts +1 -0
- package/dist/http-interceptor/src/agents.test.js +35 -0
- package/dist/http-interceptor/src/idempotency.d.ts +68 -0
- package/dist/http-interceptor/src/idempotency.js +149 -0
- package/dist/http-interceptor/src/idempotency.test.d.ts +1 -0
- package/dist/http-interceptor/src/idempotency.test.js +75 -0
- package/dist/http-interceptor/src/index.d.ts +10 -0
- package/dist/http-interceptor/src/index.js +870 -0
- package/dist/http-interceptor/src/index.test.d.ts +1 -0
- package/dist/http-interceptor/src/index.test.js +131 -0
- package/dist/http-interceptor/src/state.d.ts +17 -0
- package/dist/http-interceptor/src/state.js +188 -0
- package/dist/logger/src/index.d.ts +2 -0
- package/dist/logger/src/index.js +18 -0
- package/dist/payment-adapters/src/index.d.ts +27 -0
- package/dist/payment-adapters/src/index.js +512 -0
- package/dist/policy-engine/src/index.d.ts +8 -0
- package/dist/policy-engine/src/index.js +90 -0
- package/dist/router/src/index.d.ts +23 -0
- package/dist/router/src/index.js +432 -0
- package/dist/types/src/index.d.ts +343 -0
- package/dist/types/src/index.js +1 -0
- package/dist/wallet/src/index.d.ts +14 -0
- package/dist/wallet/src/index.js +250 -0
- package/package.json +56 -0
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { config } from '@xmpp/config';
|
|
2
|
+
import * as StellarSdk from '@stellar/stellar-sdk';
|
|
3
|
+
import { contract, hash, Keypair, nativeToScVal, rpc, TransactionBuilder, xdr } from '@stellar/stellar-sdk';
|
|
4
|
+
import { basicNodeSigner } from '@stellar/stellar-sdk/contract';
|
|
5
|
+
import { Mppx as MppxCharge, stellar as mppCharge } from '@stellar/mpp/charge/client';
|
|
6
|
+
import { Mppx as MppxChannel, stellar as mppChannel } from '@stellar/mpp/channel/client';
|
|
7
|
+
import { wrapFetchWithPaymentFromConfig } from '@x402/fetch';
|
|
8
|
+
import { createEd25519Signer } from '@x402/stellar';
|
|
9
|
+
import { ExactStellarScheme } from '@x402/stellar/exact/client';
|
|
10
|
+
import { XLM_SAC_TESTNET } from '@stellar/mpp';
|
|
11
|
+
import { getRouteExecutionPlan } from '@xmpp/wallet';
|
|
12
|
+
const stellarNetwork = config.network;
|
|
13
|
+
const liveFetchCache = new Map();
|
|
14
|
+
const DEFAULT_LEDGER_CLOSE_SECONDS = 5;
|
|
15
|
+
const SMART_ACCOUNT_X402_RETRY_DELAY_MS = 1500;
|
|
16
|
+
function createClassicSignatureScVal(keypair, payload) {
|
|
17
|
+
return xdr.ScVal.scvVec([
|
|
18
|
+
xdr.ScVal.scvMap([
|
|
19
|
+
new xdr.ScMapEntry({
|
|
20
|
+
key: xdr.ScVal.scvSymbol('public_key'),
|
|
21
|
+
val: xdr.ScVal.scvBytes(keypair.rawPublicKey()),
|
|
22
|
+
}),
|
|
23
|
+
new xdr.ScMapEntry({
|
|
24
|
+
key: xdr.ScVal.scvSymbol('signature'),
|
|
25
|
+
val: xdr.ScVal.scvBytes(keypair.sign(payload)),
|
|
26
|
+
}),
|
|
27
|
+
]),
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
function createDelegatedSignerScVal(publicKey) {
|
|
31
|
+
return xdr.ScVal.scvVec([
|
|
32
|
+
xdr.ScVal.scvSymbol('Delegated'),
|
|
33
|
+
xdr.ScVal.scvAddress(StellarSdk.Address.fromString(publicKey).toScAddress()),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
function createDelegatedAuthPayload(publicKey, contextRuleIds) {
|
|
37
|
+
return xdr.ScVal.scvMap([
|
|
38
|
+
new xdr.ScMapEntry({
|
|
39
|
+
key: xdr.ScVal.scvSymbol('context_rule_ids'),
|
|
40
|
+
val: xdr.ScVal.scvVec(contextRuleIds.map((id) => xdr.ScVal.scvU32(id))),
|
|
41
|
+
}),
|
|
42
|
+
new xdr.ScMapEntry({
|
|
43
|
+
key: xdr.ScVal.scvSymbol('signers'),
|
|
44
|
+
val: xdr.ScVal.scvMap([
|
|
45
|
+
new xdr.ScMapEntry({
|
|
46
|
+
key: createDelegatedSignerScVal(publicKey),
|
|
47
|
+
val: xdr.ScVal.scvBytes(Buffer.alloc(0)),
|
|
48
|
+
}),
|
|
49
|
+
]),
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
function buildSmartAccountSignaturePayload(entry, networkPassphrase) {
|
|
54
|
+
const credentials = entry.credentials().address();
|
|
55
|
+
return hash(xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(new xdr.HashIdPreimageSorobanAuthorization({
|
|
56
|
+
networkId: hash(Buffer.from(networkPassphrase)),
|
|
57
|
+
nonce: credentials.nonce(),
|
|
58
|
+
invocation: entry.rootInvocation(),
|
|
59
|
+
signatureExpirationLedger: credentials.signatureExpirationLedger(),
|
|
60
|
+
})).toXDR());
|
|
61
|
+
}
|
|
62
|
+
function buildSmartAccountAuthDigest(signaturePayload, contextRuleIds) {
|
|
63
|
+
const contextRuleIdsXdr = xdr.ScVal.scvVec(contextRuleIds.map((id) => xdr.ScVal.scvU32(id))).toXDR();
|
|
64
|
+
return hash(Buffer.concat([signaturePayload, contextRuleIdsXdr]));
|
|
65
|
+
}
|
|
66
|
+
function signDelegatedSmartAccountAuth(authEntries, smartAccountContractId, delegatedSigner, expiration, networkPassphrase, contextRuleIds = [0]) {
|
|
67
|
+
const signedAuthEntries = [];
|
|
68
|
+
for (const entry of authEntries) {
|
|
69
|
+
const credentials = entry.credentials();
|
|
70
|
+
if (credentials.switch().name !== 'sorobanCredentialsAddress') {
|
|
71
|
+
signedAuthEntries.push(entry);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const authAddress = StellarSdk.Address.fromScAddress(credentials.address().address()).toString();
|
|
75
|
+
if (authAddress !== smartAccountContractId) {
|
|
76
|
+
signedAuthEntries.push(entry);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const smartAccountEntry = xdr.SorobanAuthorizationEntry.fromXDR(entry.toXDR());
|
|
80
|
+
smartAccountEntry.credentials().address().signatureExpirationLedger(expiration);
|
|
81
|
+
const authPayload = createDelegatedAuthPayload(delegatedSigner.publicKey(), contextRuleIds);
|
|
82
|
+
smartAccountEntry
|
|
83
|
+
.credentials()
|
|
84
|
+
.address()
|
|
85
|
+
.signature(authPayload);
|
|
86
|
+
signedAuthEntries.push(smartAccountEntry);
|
|
87
|
+
const signaturePayload = buildSmartAccountSignaturePayload(smartAccountEntry, networkPassphrase);
|
|
88
|
+
const authDigest = buildSmartAccountAuthDigest(signaturePayload, contextRuleIds);
|
|
89
|
+
const delegatedNonce = xdr.Int64.fromString(Date.now().toString());
|
|
90
|
+
const delegatedInvocation = new xdr.SorobanAuthorizedInvocation({
|
|
91
|
+
function: xdr.SorobanAuthorizedFunction.sorobanAuthorizedFunctionTypeContractFn(new xdr.InvokeContractArgs({
|
|
92
|
+
contractAddress: StellarSdk.Address.fromString(smartAccountContractId).toScAddress(),
|
|
93
|
+
functionName: '__check_auth',
|
|
94
|
+
args: [xdr.ScVal.scvBytes(authDigest)],
|
|
95
|
+
})),
|
|
96
|
+
subInvocations: [],
|
|
97
|
+
});
|
|
98
|
+
const delegatedPreimage = xdr.HashIdPreimage.envelopeTypeSorobanAuthorization(new xdr.HashIdPreimageSorobanAuthorization({
|
|
99
|
+
networkId: hash(Buffer.from(networkPassphrase)),
|
|
100
|
+
nonce: delegatedNonce,
|
|
101
|
+
signatureExpirationLedger: expiration,
|
|
102
|
+
invocation: delegatedInvocation,
|
|
103
|
+
}));
|
|
104
|
+
signedAuthEntries.push(new xdr.SorobanAuthorizationEntry({
|
|
105
|
+
credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(new xdr.SorobanAddressCredentials({
|
|
106
|
+
address: StellarSdk.Address.fromString(delegatedSigner.publicKey()).toScAddress(),
|
|
107
|
+
nonce: delegatedNonce,
|
|
108
|
+
signatureExpirationLedger: expiration,
|
|
109
|
+
signature: createClassicSignatureScVal(delegatedSigner, hash(delegatedPreimage.toXDR())),
|
|
110
|
+
})),
|
|
111
|
+
rootInvocation: delegatedInvocation,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
return signedAuthEntries;
|
|
115
|
+
}
|
|
116
|
+
export const __smartAccountTestUtils = {
|
|
117
|
+
createClassicSignatureScVal,
|
|
118
|
+
createDelegatedSignerScVal,
|
|
119
|
+
createDelegatedAuthPayload,
|
|
120
|
+
buildSmartAccountSignaturePayload,
|
|
121
|
+
buildSmartAccountAuthDigest,
|
|
122
|
+
signDelegatedSmartAccountAuth,
|
|
123
|
+
};
|
|
124
|
+
class SmartAccountExactStellarScheme {
|
|
125
|
+
smartAccountContractId;
|
|
126
|
+
delegatedSigner;
|
|
127
|
+
scheme = 'exact';
|
|
128
|
+
constructor(smartAccountContractId, delegatedSigner) {
|
|
129
|
+
this.smartAccountContractId = smartAccountContractId;
|
|
130
|
+
this.delegatedSigner = delegatedSigner;
|
|
131
|
+
}
|
|
132
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
133
|
+
const { scheme, network, payTo, asset, amount, maxTimeoutSeconds, extra } = paymentRequirements;
|
|
134
|
+
if (scheme !== 'exact') {
|
|
135
|
+
throw new Error(`Unsupported Stellar payment scheme: ${scheme}`);
|
|
136
|
+
}
|
|
137
|
+
if (network !== stellarNetwork) {
|
|
138
|
+
throw new Error(`Unsupported Stellar network for smart-account exact payments: ${network}`);
|
|
139
|
+
}
|
|
140
|
+
if (!extra.areFeesSponsored) {
|
|
141
|
+
throw new Error('Exact scheme requires areFeesSponsored to be true');
|
|
142
|
+
}
|
|
143
|
+
const rpcServer = new rpc.Server(config.rpcUrl);
|
|
144
|
+
const latestLedger = await rpcServer.getLatestLedger();
|
|
145
|
+
const maxLedger = latestLedger.sequence + Math.ceil(maxTimeoutSeconds / DEFAULT_LEDGER_CLOSE_SECONDS);
|
|
146
|
+
const tx = await contract.AssembledTransaction.build({
|
|
147
|
+
contractId: asset,
|
|
148
|
+
method: 'transfer',
|
|
149
|
+
args: [
|
|
150
|
+
nativeToScVal(this.smartAccountContractId, { type: 'address' }),
|
|
151
|
+
nativeToScVal(payTo, { type: 'address' }),
|
|
152
|
+
nativeToScVal(amount, { type: 'i128' }),
|
|
153
|
+
],
|
|
154
|
+
networkPassphrase: config.networkPassphrase,
|
|
155
|
+
rpcUrl: config.rpcUrl,
|
|
156
|
+
parseResultXdr: (result) => result,
|
|
157
|
+
});
|
|
158
|
+
this.ensureSimulationSucceeded(tx.simulation);
|
|
159
|
+
const missingSigners = tx.needsNonInvokerSigningBy();
|
|
160
|
+
if (!missingSigners.includes(this.smartAccountContractId) || missingSigners.length > 1) {
|
|
161
|
+
throw new Error(`Expected to sign with [${this.smartAccountContractId}], but got [${missingSigners.join(', ')}]`);
|
|
162
|
+
}
|
|
163
|
+
const builtTx = tx.built;
|
|
164
|
+
const operationXdr = builtTx?._tx.operations()[0];
|
|
165
|
+
if (!operationXdr) {
|
|
166
|
+
throw new Error('Expected an invokeHostFunction operation for smart-account exact payments.');
|
|
167
|
+
}
|
|
168
|
+
const signedAuthEntries = signDelegatedSmartAccountAuth(operationXdr.body().invokeHostFunctionOp().auth() ?? [], this.smartAccountContractId, this.delegatedSigner, maxLedger, config.networkPassphrase);
|
|
169
|
+
const signedOperationXdr = xdr.Operation.fromXDR(operationXdr.toXDR());
|
|
170
|
+
signedOperationXdr.body().invokeHostFunctionOp().auth(signedAuthEntries);
|
|
171
|
+
const signedBuilder = TransactionBuilder.cloneFrom(builtTx, {
|
|
172
|
+
networkPassphrase: config.networkPassphrase,
|
|
173
|
+
});
|
|
174
|
+
signedBuilder.clearOperations();
|
|
175
|
+
signedBuilder.addOperation(signedOperationXdr);
|
|
176
|
+
const signedTx = signedBuilder.build();
|
|
177
|
+
const resimulation = await rpcServer.simulateTransaction(signedTx);
|
|
178
|
+
this.ensureSimulationSucceeded(resimulation);
|
|
179
|
+
const preparedTx = rpc.assembleTransaction(signedTx, resimulation).build();
|
|
180
|
+
return {
|
|
181
|
+
x402Version,
|
|
182
|
+
payload: {
|
|
183
|
+
transaction: preparedTx.toXDR(),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
ensureSimulationSucceeded(simulation) {
|
|
188
|
+
if (!simulation) {
|
|
189
|
+
throw new Error('Simulation result is undefined');
|
|
190
|
+
}
|
|
191
|
+
if (rpc.Api.isSimulationRestore(simulation)) {
|
|
192
|
+
throw new Error(`Stellar simulation requested restore: ${simulation.restorePreamble}`);
|
|
193
|
+
}
|
|
194
|
+
if (rpc.Api.isSimulationError(simulation)) {
|
|
195
|
+
throw new Error(`Stellar simulation failed${simulation.error ? ` with error message: ${simulation.error}` : ''}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
export function buildRetryHeaders(challenge, route) {
|
|
200
|
+
return {
|
|
201
|
+
[challenge.retryHeaderName]: challenge.retryHeaderValue,
|
|
202
|
+
'x-xmpp-route': route,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function createReceiptId(route) {
|
|
206
|
+
return `xmpp_${route}_${Date.now().toString(36)}`;
|
|
207
|
+
}
|
|
208
|
+
function requiredConfigForRoute(route) {
|
|
209
|
+
if (route === 'x402') {
|
|
210
|
+
return ['XMPP_AGENT_SECRET_KEY', 'FACILITATOR_STELLAR_PRIVATE_KEY'];
|
|
211
|
+
}
|
|
212
|
+
if (route === 'mpp-session-open' || route === 'mpp-session-reuse') {
|
|
213
|
+
return ['XMPP_AGENT_SECRET_KEY', 'MPP_SECRET_KEY', 'MPP_CHANNEL_CONTRACT_ID'];
|
|
214
|
+
}
|
|
215
|
+
return ['XMPP_AGENT_SECRET_KEY', 'MPP_SECRET_KEY'];
|
|
216
|
+
}
|
|
217
|
+
function hasEnvVar(name) {
|
|
218
|
+
return Boolean(process.env[name]?.trim());
|
|
219
|
+
}
|
|
220
|
+
function getExecutionStatus(route) {
|
|
221
|
+
const requestedMode = process.env.XMPP_PAYMENT_EXECUTION_MODE;
|
|
222
|
+
const mode = requestedMode === 'mock' || requestedMode === 'testnet'
|
|
223
|
+
? requestedMode
|
|
224
|
+
: config.paymentExecutionMode;
|
|
225
|
+
if (mode === 'mock') {
|
|
226
|
+
return {
|
|
227
|
+
mode,
|
|
228
|
+
status: 'mock-paid',
|
|
229
|
+
missingConfig: [],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const missingConfig = requiredConfigForRoute(route).filter((name) => !hasEnvVar(name));
|
|
233
|
+
return {
|
|
234
|
+
mode,
|
|
235
|
+
status: missingConfig.length === 0 ? 'ready-for-testnet' : 'missing-config',
|
|
236
|
+
missingConfig,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
export function preparePaymentExecution(challenge, route) {
|
|
240
|
+
const receiptId = createReceiptId(route);
|
|
241
|
+
const { mode, status, missingConfig } = getExecutionStatus(route);
|
|
242
|
+
const executionPlan = getRouteExecutionPlan(route);
|
|
243
|
+
return {
|
|
244
|
+
headers: {
|
|
245
|
+
...buildRetryHeaders(challenge, route),
|
|
246
|
+
'x-xmpp-receipt': receiptId,
|
|
247
|
+
'x-xmpp-execution-mode': mode,
|
|
248
|
+
'x-xmpp-execution-status': status,
|
|
249
|
+
},
|
|
250
|
+
metadata: {
|
|
251
|
+
mode,
|
|
252
|
+
status,
|
|
253
|
+
route,
|
|
254
|
+
receiptId,
|
|
255
|
+
missingConfig: missingConfig.length > 0 ? missingConfig : undefined,
|
|
256
|
+
settlementStrategy: executionPlan.settlementStrategy,
|
|
257
|
+
executionNote: executionPlan.executionNote,
|
|
258
|
+
smartAccount: executionPlan.smartAccount,
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function getAgentSecretKey() {
|
|
263
|
+
if (!config.wallet.agentSecretKey) {
|
|
264
|
+
throw new Error('XMPP_AGENT_SECRET_KEY is required for live payment execution.');
|
|
265
|
+
}
|
|
266
|
+
return config.wallet.agentSecretKey;
|
|
267
|
+
}
|
|
268
|
+
function createX402ClientSigner(preferSmartAccount) {
|
|
269
|
+
if (preferSmartAccount && config.wallet.smartAccountContractId) {
|
|
270
|
+
const keypair = Keypair.fromSecret(getAgentSecretKey());
|
|
271
|
+
const signer = basicNodeSigner(keypair, config.networkPassphrase);
|
|
272
|
+
return {
|
|
273
|
+
address: config.wallet.smartAccountContractId,
|
|
274
|
+
signAuthEntry: async (authEntry) => ({
|
|
275
|
+
signedAuthEntry: keypair.sign(hash(Buffer.from(authEntry, 'base64'))).toString('base64'),
|
|
276
|
+
signerAddress: config.wallet.smartAccountContractId,
|
|
277
|
+
}),
|
|
278
|
+
signTransaction: signer.signTransaction,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
return createEd25519Signer(getAgentSecretKey(), stellarNetwork);
|
|
282
|
+
}
|
|
283
|
+
function createX402Scheme(preferSmartAccount) {
|
|
284
|
+
if (preferSmartAccount && config.wallet.smartAccountContractId) {
|
|
285
|
+
return new SmartAccountExactStellarScheme(config.wallet.smartAccountContractId, Keypair.fromSecret(getAgentSecretKey()));
|
|
286
|
+
}
|
|
287
|
+
return new ExactStellarScheme(createX402ClientSigner(false), { url: config.rpcUrl });
|
|
288
|
+
}
|
|
289
|
+
function getLiveFetchForRoute(route, options) {
|
|
290
|
+
if (route === 'x402') {
|
|
291
|
+
const cacheKey = options?.preferSmartAccount === false ? 'x402-keypair' : 'x402-smart-account';
|
|
292
|
+
const cached = liveFetchCache.get(cacheKey);
|
|
293
|
+
if (cached) {
|
|
294
|
+
return cached;
|
|
295
|
+
}
|
|
296
|
+
const paidFetch = wrapFetchWithPaymentFromConfig(globalThis.fetch, {
|
|
297
|
+
schemes: [
|
|
298
|
+
{
|
|
299
|
+
network: 'stellar:*',
|
|
300
|
+
client: createX402Scheme(options?.preferSmartAccount !== false),
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
liveFetchCache.set(cacheKey, paidFetch);
|
|
305
|
+
return paidFetch;
|
|
306
|
+
}
|
|
307
|
+
if (route === 'mpp-charge') {
|
|
308
|
+
const cached = liveFetchCache.get('mpp-charge');
|
|
309
|
+
if (cached) {
|
|
310
|
+
return cached;
|
|
311
|
+
}
|
|
312
|
+
const mppx = MppxCharge.create({
|
|
313
|
+
methods: [
|
|
314
|
+
mppCharge.charge({
|
|
315
|
+
keypair: Keypair.fromSecret(getAgentSecretKey()),
|
|
316
|
+
rpcUrl: config.rpcUrl,
|
|
317
|
+
}),
|
|
318
|
+
],
|
|
319
|
+
polyfill: false,
|
|
320
|
+
});
|
|
321
|
+
liveFetchCache.set('mpp-charge', mppx.fetch);
|
|
322
|
+
return mppx.fetch;
|
|
323
|
+
}
|
|
324
|
+
const cached = liveFetchCache.get('mpp-session');
|
|
325
|
+
if (cached) {
|
|
326
|
+
return cached;
|
|
327
|
+
}
|
|
328
|
+
const agentKeypair = Keypair.fromSecret(getAgentSecretKey());
|
|
329
|
+
const mppx = MppxChannel.create({
|
|
330
|
+
methods: [
|
|
331
|
+
mppChannel.channel({
|
|
332
|
+
commitmentKey: agentKeypair,
|
|
333
|
+
sourceAccount: agentKeypair.publicKey(),
|
|
334
|
+
rpcUrl: config.rpcUrl,
|
|
335
|
+
}),
|
|
336
|
+
],
|
|
337
|
+
polyfill: false,
|
|
338
|
+
});
|
|
339
|
+
liveFetchCache.set('mpp-session', mppx.fetch);
|
|
340
|
+
return mppx.fetch;
|
|
341
|
+
}
|
|
342
|
+
function createExecutionMetadata(route) {
|
|
343
|
+
const receiptId = createReceiptId(route);
|
|
344
|
+
const { mode, status, missingConfig } = getExecutionStatus(route);
|
|
345
|
+
const executionPlan = getRouteExecutionPlan(route);
|
|
346
|
+
return {
|
|
347
|
+
mode,
|
|
348
|
+
status,
|
|
349
|
+
route,
|
|
350
|
+
receiptId,
|
|
351
|
+
missingConfig: missingConfig.length > 0 ? missingConfig : undefined,
|
|
352
|
+
settlementStrategy: executionPlan.settlementStrategy,
|
|
353
|
+
executionNote: executionPlan.executionNote,
|
|
354
|
+
smartAccount: executionPlan.smartAccount,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function createSmartAccountFallbackMetadata(metadata, error) {
|
|
358
|
+
const fallbackReason = error instanceof Error && error.message
|
|
359
|
+
? `Smart-account x402 failed and xMPP fell back to keypair settlement: ${error.message}`
|
|
360
|
+
: 'Smart-account x402 failed and xMPP fell back to keypair settlement.';
|
|
361
|
+
return {
|
|
362
|
+
...metadata,
|
|
363
|
+
settlementStrategy: 'keypair-fallback',
|
|
364
|
+
executionNote: 'x402 automatically fell back to the stable keypair path after a smart-account execution failure.',
|
|
365
|
+
smartAccount: metadata.smartAccount
|
|
366
|
+
? {
|
|
367
|
+
...metadata.smartAccount,
|
|
368
|
+
used: false,
|
|
369
|
+
fallbackReason,
|
|
370
|
+
}
|
|
371
|
+
: undefined,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function extractEvidenceHeaders(headers) {
|
|
375
|
+
const evidenceEntries = [];
|
|
376
|
+
headers.forEach((value, key) => {
|
|
377
|
+
const normalized = key.toLowerCase();
|
|
378
|
+
if (normalized.startsWith('x-payment') ||
|
|
379
|
+
normalized.startsWith('x-mpp') ||
|
|
380
|
+
normalized.startsWith('x-xmpp-') ||
|
|
381
|
+
normalized.includes('receipt') ||
|
|
382
|
+
normalized === 'payment-response' ||
|
|
383
|
+
normalized.includes('transaction')) {
|
|
384
|
+
evidenceEntries.push([key, value]);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
if (evidenceEntries.length === 0) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
return Object.fromEntries(evidenceEntries);
|
|
391
|
+
}
|
|
392
|
+
function getBooleanHeader(headers, name) {
|
|
393
|
+
const value = headers.get(name);
|
|
394
|
+
if (value == null) {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
return value.toLowerCase() === 'true';
|
|
398
|
+
}
|
|
399
|
+
async function retrySmartAccountX402Fetch(input, init, paidFetch) {
|
|
400
|
+
await new Promise((resolve) => setTimeout(resolve, SMART_ACCOUNT_X402_RETRY_DELAY_MS));
|
|
401
|
+
return paidFetch(input, init);
|
|
402
|
+
}
|
|
403
|
+
export async function executePaymentRoute(route, input, init) {
|
|
404
|
+
const metadata = createExecutionMetadata(route);
|
|
405
|
+
if (metadata.status === 'missing-config') {
|
|
406
|
+
return {
|
|
407
|
+
response: new Response(JSON.stringify({
|
|
408
|
+
error: 'xMPP live payment execution is not fully configured for this route.',
|
|
409
|
+
route,
|
|
410
|
+
missingConfig: metadata.missingConfig ?? [],
|
|
411
|
+
}), {
|
|
412
|
+
status: 424,
|
|
413
|
+
headers: { 'content-type': 'application/json' },
|
|
414
|
+
}),
|
|
415
|
+
metadata,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
if (route === 'mpp-session-open' || route === 'mpp-session-reuse') {
|
|
419
|
+
if (!config.mpp.channelContractId) {
|
|
420
|
+
return {
|
|
421
|
+
response: new Response(JSON.stringify({
|
|
422
|
+
error: 'MPP session mode requires a deployed one-way-channel contract.',
|
|
423
|
+
route,
|
|
424
|
+
}), {
|
|
425
|
+
status: 424,
|
|
426
|
+
headers: { 'content-type': 'application/json' },
|
|
427
|
+
}),
|
|
428
|
+
metadata: {
|
|
429
|
+
...metadata,
|
|
430
|
+
status: 'missing-config',
|
|
431
|
+
missingConfig: ['MPP_CHANNEL_CONTRACT_ID'],
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const paidFetch = getLiveFetchForRoute(route);
|
|
437
|
+
const liveHeaders = new Headers(init?.headers);
|
|
438
|
+
liveHeaders.set('x-xmpp-route', route);
|
|
439
|
+
const liveInit = {
|
|
440
|
+
...init,
|
|
441
|
+
headers: liveHeaders,
|
|
442
|
+
};
|
|
443
|
+
let response;
|
|
444
|
+
let finalMetadata = metadata;
|
|
445
|
+
try {
|
|
446
|
+
response = await paidFetch(input, liveInit);
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (route !== 'x402' || !metadata.smartAccount?.used) {
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
try {
|
|
453
|
+
response = await retrySmartAccountX402Fetch(input, liveInit, paidFetch);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
finalMetadata = createSmartAccountFallbackMetadata(metadata, error);
|
|
457
|
+
response = await getLiveFetchForRoute('x402', { preferSmartAccount: false })(input, liveInit);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (route === 'x402' && metadata.smartAccount?.used && response.status === 402) {
|
|
461
|
+
const retryResponse = await retrySmartAccountX402Fetch(input, liveInit, paidFetch);
|
|
462
|
+
if (retryResponse.status !== 402) {
|
|
463
|
+
response = retryResponse;
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
finalMetadata = createSmartAccountFallbackMetadata(metadata, new Error('Smart-account x402 returned an unresolved 402 challenge.'));
|
|
467
|
+
response = await getLiveFetchForRoute('x402', { preferSmartAccount: false })(input, liveInit);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const headers = new Headers(response.headers);
|
|
471
|
+
const evidenceHeaders = extractEvidenceHeaders(headers);
|
|
472
|
+
finalMetadata = {
|
|
473
|
+
...finalMetadata,
|
|
474
|
+
status: response.status === 402 ? metadata.status : 'settled-testnet',
|
|
475
|
+
evidenceHeaders,
|
|
476
|
+
feeSponsored: getBooleanHeader(headers, 'x-xmpp-fee-sponsored'),
|
|
477
|
+
feeSponsorPublicKey: headers.get('x-xmpp-fee-sponsor') ?? undefined,
|
|
478
|
+
feeBumpPublicKey: headers.get('x-xmpp-fee-bump-sponsor') ?? undefined,
|
|
479
|
+
};
|
|
480
|
+
headers.set('x-xmpp-receipt', metadata.receiptId);
|
|
481
|
+
headers.set('x-xmpp-execution-mode', metadata.mode);
|
|
482
|
+
headers.set('x-xmpp-execution-status', finalMetadata.status);
|
|
483
|
+
headers.set('x-xmpp-route', route);
|
|
484
|
+
if (finalMetadata.settlementStrategy) {
|
|
485
|
+
headers.set('x-xmpp-settlement-strategy', finalMetadata.settlementStrategy);
|
|
486
|
+
}
|
|
487
|
+
if (finalMetadata.smartAccount) {
|
|
488
|
+
headers.set('x-xmpp-smart-account-used', String(finalMetadata.smartAccount.used));
|
|
489
|
+
if (finalMetadata.smartAccount.fallbackReason) {
|
|
490
|
+
headers.set('x-xmpp-smart-account-fallback', finalMetadata.smartAccount.fallbackReason);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (typeof finalMetadata.feeSponsored === 'boolean') {
|
|
494
|
+
headers.set('x-xmpp-fee-sponsored', String(finalMetadata.feeSponsored));
|
|
495
|
+
}
|
|
496
|
+
if (finalMetadata.feeSponsorPublicKey) {
|
|
497
|
+
headers.set('x-xmpp-fee-sponsor', finalMetadata.feeSponsorPublicKey);
|
|
498
|
+
}
|
|
499
|
+
if (finalMetadata.feeBumpPublicKey) {
|
|
500
|
+
headers.set('x-xmpp-fee-bump-sponsor', finalMetadata.feeBumpPublicKey);
|
|
501
|
+
}
|
|
502
|
+
const body = await response.arrayBuffer();
|
|
503
|
+
return {
|
|
504
|
+
response: new Response(body, {
|
|
505
|
+
status: response.status,
|
|
506
|
+
statusText: response.statusText,
|
|
507
|
+
headers,
|
|
508
|
+
}),
|
|
509
|
+
metadata: finalMetadata,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
export { XLM_SAC_TESTNET };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PolicyDecision } from '@xmpp/types';
|
|
2
|
+
export declare function isAllowedDomain(url: string): boolean;
|
|
3
|
+
export declare function evaluatePolicy(url: string): PolicyDecision;
|
|
4
|
+
export declare function evaluatePolicyForRequest(input: {
|
|
5
|
+
url: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
serviceId?: string;
|
|
8
|
+
}): Promise<PolicyDecision>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getPolicyRuntimeSnapshot } from '@xmpp/contract-runtime';
|
|
2
|
+
const allowedHosts = new Set(['localhost', '127.0.0.1']);
|
|
3
|
+
const blockedPathPrefixes = ['/admin', '/internal', '/unsafe'];
|
|
4
|
+
export function isAllowedDomain(url) {
|
|
5
|
+
const parsed = new URL(url);
|
|
6
|
+
return allowedHosts.has(parsed.hostname);
|
|
7
|
+
}
|
|
8
|
+
export function evaluatePolicy(url) {
|
|
9
|
+
const parsed = new URL(url);
|
|
10
|
+
if (!allowedHosts.has(parsed.hostname)) {
|
|
11
|
+
return {
|
|
12
|
+
allowed: false,
|
|
13
|
+
reason: 'xMPP policy allows automatic payment only to approved local demo hosts.',
|
|
14
|
+
code: 'blocked-domain',
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
if (blockedPathPrefixes.some((prefix) => parsed.pathname.startsWith(prefix))) {
|
|
18
|
+
return {
|
|
19
|
+
allowed: false,
|
|
20
|
+
reason: 'xMPP policy blocks sensitive admin or unsafe routes from automatic payment.',
|
|
21
|
+
code: 'blocked-path',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
allowed: true,
|
|
26
|
+
reason: 'xMPP policy approved this request for automatic payment routing.',
|
|
27
|
+
code: 'allowed',
|
|
28
|
+
source: 'local',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export async function evaluatePolicyForRequest(input) {
|
|
32
|
+
const localDecision = evaluatePolicy(input.url);
|
|
33
|
+
if (!localDecision.allowed) {
|
|
34
|
+
return localDecision;
|
|
35
|
+
}
|
|
36
|
+
const runtime = await getPolicyRuntimeSnapshot(input.serviceId);
|
|
37
|
+
if (runtime.pauseFlag) {
|
|
38
|
+
return {
|
|
39
|
+
allowed: false,
|
|
40
|
+
reason: 'xMPP policy contract is paused for automatic payment execution.',
|
|
41
|
+
code: 'paused',
|
|
42
|
+
source: runtime.source,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const method = (input.method ?? 'GET').toUpperCase();
|
|
46
|
+
if (method !== 'GET' && input.serviceId == null) {
|
|
47
|
+
return {
|
|
48
|
+
allowed: false,
|
|
49
|
+
reason: 'xMPP requires an explicit service id before automatic payment can proceed on non-GET requests.',
|
|
50
|
+
code: 'blocked-service',
|
|
51
|
+
source: runtime.source,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (method !== 'GET' && runtime.globalPolicy == null) {
|
|
55
|
+
return {
|
|
56
|
+
allowed: false,
|
|
57
|
+
reason: 'xMPP blocks automatic payment on non-GET routes unless policy explicitly enables it.',
|
|
58
|
+
code: 'blocked-method',
|
|
59
|
+
source: runtime.source,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (method !== 'GET' && runtime.globalPolicy && !runtime.globalPolicy.allowPostAutopay) {
|
|
63
|
+
return {
|
|
64
|
+
allowed: false,
|
|
65
|
+
reason: 'xMPP policy blocks automatic payment on non-GET routes unless explicitly enabled.',
|
|
66
|
+
code: 'blocked-method',
|
|
67
|
+
source: runtime.source,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (input.serviceId == null && runtime.globalPolicy && !runtime.globalPolicy.allowUnknownServices) {
|
|
71
|
+
return {
|
|
72
|
+
allowed: false,
|
|
73
|
+
reason: 'xMPP policy requires a known service id before automatic payment can proceed.',
|
|
74
|
+
code: 'blocked-service',
|
|
75
|
+
source: runtime.source,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (runtime.servicePolicy && !runtime.servicePolicy.enabled) {
|
|
79
|
+
return {
|
|
80
|
+
allowed: false,
|
|
81
|
+
reason: 'xMPP service policy disabled automatic payment for this service.',
|
|
82
|
+
code: 'blocked-service',
|
|
83
|
+
source: runtime.source,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
...localDecision,
|
|
88
|
+
source: runtime.source,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PaymentChallenge, RouteContext, RouteDecision, RouteKind, ServiceCatalogEntry, WorkflowEstimateResult, WorkflowEstimateStep } from '@xmpp/types';
|
|
2
|
+
export declare function resolveCatalogEntry(input: RouteContext): ServiceCatalogEntry;
|
|
3
|
+
export declare function estimateRouteCost(input: {
|
|
4
|
+
route: RouteKind;
|
|
5
|
+
url: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
serviceId?: string;
|
|
8
|
+
projectedRequests?: number;
|
|
9
|
+
hasReusableSession?: boolean;
|
|
10
|
+
}): number;
|
|
11
|
+
export declare function getServiceCatalog(): ServiceCatalogEntry[];
|
|
12
|
+
export declare function getServiceCatalogEntry(serviceId: string): ServiceCatalogEntry | null;
|
|
13
|
+
export declare function createRouter(): {
|
|
14
|
+
preview(input: RouteContext): Promise<RouteDecision>;
|
|
15
|
+
chooseFromChallenge(input: RouteContext & {
|
|
16
|
+
challenge: PaymentChallenge;
|
|
17
|
+
hasReusableSession?: boolean;
|
|
18
|
+
}): Promise<RouteDecision>;
|
|
19
|
+
explain(input: RouteContext & {
|
|
20
|
+
hasReusableSession?: boolean;
|
|
21
|
+
}): RouteDecision;
|
|
22
|
+
estimateWorkflow(steps: WorkflowEstimateStep[]): WorkflowEstimateResult;
|
|
23
|
+
};
|