@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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/dist/config/src/index.d.ts +49 -0
  3. package/dist/config/src/index.js +117 -0
  4. package/dist/contract-runtime/src/index.d.ts +44 -0
  5. package/dist/contract-runtime/src/index.js +364 -0
  6. package/dist/http-interceptor/src/agents.d.ts +5 -0
  7. package/dist/http-interceptor/src/agents.js +88 -0
  8. package/dist/http-interceptor/src/agents.test.d.ts +1 -0
  9. package/dist/http-interceptor/src/agents.test.js +35 -0
  10. package/dist/http-interceptor/src/idempotency.d.ts +68 -0
  11. package/dist/http-interceptor/src/idempotency.js +149 -0
  12. package/dist/http-interceptor/src/idempotency.test.d.ts +1 -0
  13. package/dist/http-interceptor/src/idempotency.test.js +75 -0
  14. package/dist/http-interceptor/src/index.d.ts +10 -0
  15. package/dist/http-interceptor/src/index.js +870 -0
  16. package/dist/http-interceptor/src/index.test.d.ts +1 -0
  17. package/dist/http-interceptor/src/index.test.js +131 -0
  18. package/dist/http-interceptor/src/state.d.ts +17 -0
  19. package/dist/http-interceptor/src/state.js +188 -0
  20. package/dist/logger/src/index.d.ts +2 -0
  21. package/dist/logger/src/index.js +18 -0
  22. package/dist/payment-adapters/src/index.d.ts +27 -0
  23. package/dist/payment-adapters/src/index.js +512 -0
  24. package/dist/policy-engine/src/index.d.ts +8 -0
  25. package/dist/policy-engine/src/index.js +90 -0
  26. package/dist/router/src/index.d.ts +23 -0
  27. package/dist/router/src/index.js +432 -0
  28. package/dist/types/src/index.d.ts +343 -0
  29. package/dist/types/src/index.js +1 -0
  30. package/dist/wallet/src/index.d.ts +14 -0
  31. package/dist/wallet/src/index.js +250 -0
  32. package/package.json +56 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,131 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as paymentAdapters from '@xmpp/payment-adapters';
3
+ import { getXmppMetadata, resetXmppRuntimeState, xmppFetch } from './index.js';
4
+ describe('xmppFetch', () => {
5
+ beforeEach(() => {
6
+ resetXmppRuntimeState();
7
+ });
8
+ afterEach(() => {
9
+ vi.restoreAllMocks();
10
+ resetXmppRuntimeState();
11
+ });
12
+ it('retries after a 402 payment challenge', async () => {
13
+ process.env.XMPP_PAYMENT_EXECUTION_MODE = 'mock';
14
+ const fetchMock = vi
15
+ .spyOn(globalThis, 'fetch')
16
+ .mockResolvedValueOnce(new Response(JSON.stringify({
17
+ kind: 'x402',
18
+ service: 'research-api',
19
+ amountUsd: 0.01,
20
+ retryHeaderName: 'x-xmpp-paid',
21
+ retryHeaderValue: 'ok',
22
+ }), {
23
+ status: 402,
24
+ headers: { 'content-type': 'application/json' },
25
+ }))
26
+ .mockResolvedValueOnce(new Response(JSON.stringify({ ok: true }), { status: 200 }));
27
+ const response = await xmppFetch('http://localhost:4101/research?q=stellar');
28
+ const metadata = getXmppMetadata(response);
29
+ expect(response.status).toBe(200);
30
+ expect(metadata?.retried).toBe(true);
31
+ expect(metadata?.execution?.status).toBeDefined();
32
+ expect(metadata?.execution?.status).not.toBe('missing-config');
33
+ expect(fetchMock).toHaveBeenCalledTimes(2);
34
+ });
35
+ it('stops before retry when testnet mode is missing secrets', async () => {
36
+ process.env.XMPP_PAYMENT_EXECUTION_MODE = 'testnet';
37
+ delete process.env.XMPP_AGENT_SECRET_KEY;
38
+ delete process.env.MPP_SECRET_KEY;
39
+ const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(new Response(JSON.stringify({
40
+ kind: 'mpp-charge',
41
+ service: 'market-api',
42
+ amountUsd: 0.03,
43
+ retryHeaderName: 'x-xmpp-paid',
44
+ retryHeaderValue: 'ok',
45
+ }), {
46
+ status: 402,
47
+ headers: { 'content-type': 'application/json' },
48
+ }));
49
+ const response = await xmppFetch('http://localhost:4102/quote?symbol=XLM', undefined, {
50
+ serviceId: 'market-api',
51
+ });
52
+ const metadata = getXmppMetadata(response);
53
+ expect(response.status).toBe(424);
54
+ expect(metadata?.retried).toBe(false);
55
+ expect(metadata?.execution?.status).toBe('missing-config');
56
+ expect(metadata?.execution?.missingConfig).toContain('XMPP_AGENT_SECRET_KEY');
57
+ expect(metadata?.execution?.missingConfig).toContain('MPP_SECRET_KEY');
58
+ expect(fetchMock).toHaveBeenCalledTimes(0);
59
+ });
60
+ it('blocks unsafe routes before any network request', async () => {
61
+ process.env.XMPP_PAYMENT_EXECUTION_MODE = 'mock';
62
+ const fetchMock = vi.spyOn(globalThis, 'fetch');
63
+ const response = await xmppFetch('http://localhost:4102/admin/export');
64
+ const metadata = getXmppMetadata(response);
65
+ const body = await response.json();
66
+ expect(response.status).toBe(403);
67
+ expect(body.error).toContain('policy denied');
68
+ expect(metadata?.policy?.allowed).toBe(false);
69
+ expect(metadata?.policy?.code).toBe('blocked-path');
70
+ expect(fetchMock).not.toHaveBeenCalled();
71
+ });
72
+ it('blocks an agent from spending against a disallowed service', async () => {
73
+ process.env.XMPP_PAYMENT_EXECUTION_MODE = 'mock';
74
+ const fetchMock = vi.spyOn(globalThis, 'fetch');
75
+ const response = await xmppFetch('http://localhost:4102/quote?symbol=XLM', undefined, {
76
+ agentId: 'research-agent',
77
+ serviceId: 'market-api',
78
+ projectedRequests: 1,
79
+ });
80
+ const metadata = getXmppMetadata(response);
81
+ const body = await response.json();
82
+ expect(response.status).toBe(403);
83
+ expect(body.error).toContain('agent policy denied');
84
+ expect(metadata?.policy?.code).toBe('blocked-agent');
85
+ expect(fetchMock).not.toHaveBeenCalled();
86
+ });
87
+ it('opens then reuses a session in testnet mode and clears it on runtime reset', async () => {
88
+ process.env.XMPP_PAYMENT_EXECUTION_MODE = 'testnet';
89
+ process.env.XMPP_AGENT_SECRET_KEY =
90
+ 'SDJ5L6UNCSFK5L2AUIIRVIE7KW4HAZ6N7P2KQ6CUPT5KUG6J6LYW7CLW';
91
+ process.env.MPP_SECRET_KEY =
92
+ 'SB5Y5P5ONLTHYBUFSUWPG4KSTC35DEYKM3NOMFEML3A5FC4T5TXV6H3V';
93
+ process.env.MPP_CHANNEL_CONTRACT_ID =
94
+ 'CDCMWMSCRL2HR5YZLMFKLWR5DEPTAEOOLKBXYLQ2MK4FBPUOVUP72E3A';
95
+ const executePaymentRoute = vi
96
+ .spyOn(paymentAdapters, 'executePaymentRoute')
97
+ .mockImplementation(async (route) => ({
98
+ response: new Response(JSON.stringify({ ok: true, route }), { status: 200 }),
99
+ metadata: {
100
+ mode: 'testnet',
101
+ status: 'settled-testnet',
102
+ route,
103
+ receiptId: `receipt-${route}`,
104
+ },
105
+ }));
106
+ const input = 'http://localhost:4103/stream/tick';
107
+ const options = {
108
+ agentId: 'market-agent',
109
+ serviceId: 'stream-api',
110
+ projectedRequests: 5,
111
+ streaming: true,
112
+ };
113
+ const first = await xmppFetch(input, { method: 'GET' }, options);
114
+ const firstMetadata = getXmppMetadata(first);
115
+ const second = await xmppFetch(input, { method: 'GET' }, options);
116
+ const secondMetadata = getXmppMetadata(second);
117
+ expect(firstMetadata?.route).toBe('mpp-session-open');
118
+ expect(firstMetadata?.execution?.route).toBe('mpp-session-open');
119
+ expect(secondMetadata?.route).toBe('mpp-session-reuse');
120
+ expect(secondMetadata?.execution?.route).toBe('mpp-session-reuse');
121
+ expect(executePaymentRoute.mock.calls.map(([route]) => route)).toEqual([
122
+ 'mpp-session-open',
123
+ 'mpp-session-reuse',
124
+ ]);
125
+ resetXmppRuntimeState();
126
+ const afterReset = await xmppFetch(input, { method: 'GET' }, options);
127
+ const resetMetadata = getXmppMetadata(afterReset);
128
+ expect(resetMetadata?.route).toBe('mpp-session-open');
129
+ expect(resetMetadata?.execution?.route).toBe('mpp-session-open');
130
+ });
131
+ });
@@ -0,0 +1,17 @@
1
+ import type { XmppAgentProfile, RouteKind, XmppAgentStateSummary, XmppBudgetSnapshot, XmppOperatorState, XmppRouteEvent, XmppSessionRecord } from '@xmpp/types';
2
+ export declare function recordXmppEvent(input: Omit<XmppRouteEvent, 'id' | 'timestamp'>): XmppRouteEvent;
3
+ export declare function upsertLocalSession(sessionId: string, serviceId: string, callCount: number): void;
4
+ export declare function listLocalSessions(): Pick<XmppSessionRecord, "serviceId" | "sessionId" | "callCount">[];
5
+ export declare function buildBudgetSnapshot(input: {
6
+ agentId?: string;
7
+ agentProfile?: XmppAgentProfile;
8
+ url: string;
9
+ method: string;
10
+ serviceId?: string;
11
+ route: RouteKind;
12
+ projectedRequests?: number;
13
+ hasReusableSession?: boolean;
14
+ }): XmppBudgetSnapshot;
15
+ export declare function buildAgentStates(profiles?: XmppAgentProfile[]): XmppAgentStateSummary[];
16
+ export declare function getXmppOperatorState(agentProfiles?: XmppAgentProfile[]): XmppOperatorState;
17
+ export declare function resetXmppOperatorState(): void;
@@ -0,0 +1,188 @@
1
+ import { config } from '@xmpp/config';
2
+ import { createRouter, estimateRouteCost, resolveCatalogEntry } from '@xmpp/router';
3
+ import { getXmppAgentProfile, listXmppAgentProfiles } from './agents.js';
4
+ const router = createRouter();
5
+ const recentEvents = [];
6
+ const serviceSpendUsd = new Map();
7
+ const serviceCallCounts = new Map();
8
+ const agentSpendUsd = new Map();
9
+ const agentServiceCallCounts = new Map();
10
+ const agentRouteCounts = new Map();
11
+ const routeCounts = {
12
+ x402: 0,
13
+ 'mpp-charge': 0,
14
+ 'mpp-session-open': 0,
15
+ 'mpp-session-reuse': 0,
16
+ };
17
+ const openSessions = new Map();
18
+ let sessionSavingsUsd = 0;
19
+ function roundUsd(value) {
20
+ return Math.round(value * 1000) / 1000;
21
+ }
22
+ function pushEvent(event) {
23
+ recentEvents.unshift(event);
24
+ if (recentEvents.length > 25) {
25
+ recentEvents.length = 25;
26
+ }
27
+ }
28
+ function emptyRouteCounts() {
29
+ return {
30
+ x402: 0,
31
+ 'mpp-charge': 0,
32
+ 'mpp-session-open': 0,
33
+ 'mpp-session-reuse': 0,
34
+ };
35
+ }
36
+ function getAgentRouteCounter(agentId) {
37
+ const existing = agentRouteCounts.get(agentId);
38
+ if (existing) {
39
+ return existing;
40
+ }
41
+ const created = emptyRouteCounts();
42
+ agentRouteCounts.set(agentId, created);
43
+ return created;
44
+ }
45
+ export function recordXmppEvent(input) {
46
+ const event = {
47
+ id: `evt_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
48
+ timestamp: new Date().toISOString(),
49
+ ...input,
50
+ amountUsd: roundUsd(input.amountUsd),
51
+ };
52
+ pushEvent(event);
53
+ if (event.status === 'settled' && event.amountUsd > 0) {
54
+ routeCounts[event.route] += 1;
55
+ getAgentRouteCounter(event.agentId)[event.route] += 1;
56
+ serviceSpendUsd.set(event.serviceId, roundUsd((serviceSpendUsd.get(event.serviceId) ?? 0) + event.amountUsd));
57
+ serviceCallCounts.set(event.serviceId, (serviceCallCounts.get(event.serviceId) ?? 0) + 1);
58
+ agentSpendUsd.set(event.agentId, roundUsd((agentSpendUsd.get(event.agentId) ?? 0) + event.amountUsd));
59
+ agentServiceCallCounts.set(`${event.agentId}:${event.serviceId}`, (agentServiceCallCounts.get(`${event.agentId}:${event.serviceId}`) ?? 0) + 1);
60
+ const naiveCost = estimateRouteCost({
61
+ route: 'x402',
62
+ url: event.url,
63
+ method: event.method,
64
+ serviceId: event.serviceId,
65
+ projectedRequests: 1,
66
+ });
67
+ sessionSavingsUsd = roundUsd(sessionSavingsUsd + Math.max(0, naiveCost - event.amountUsd));
68
+ }
69
+ return event;
70
+ }
71
+ export function upsertLocalSession(sessionId, serviceId, callCount) {
72
+ openSessions.set(sessionId, {
73
+ sessionId,
74
+ serviceId,
75
+ callCount,
76
+ });
77
+ }
78
+ export function listLocalSessions() {
79
+ return [...openSessions.values()];
80
+ }
81
+ export function buildBudgetSnapshot(input) {
82
+ const agent = input.agentProfile ?? getXmppAgentProfile(input.agentId);
83
+ const serviceId = input.serviceId ?? resolveCatalogEntry(input).serviceId;
84
+ const callsThisService = agentServiceCallCounts.get(`${agent.agentId}:${serviceId}`) ?? 0;
85
+ const agentSpentThisSessionUsd = roundUsd(agentSpendUsd.get(agent.agentId) ?? 0);
86
+ const agentRemainingDailyBudgetUsd = roundUsd(Math.max(0, agent.dailyBudgetUsd - agentSpentThisSessionUsd));
87
+ const spentThisSessionUsd = roundUsd([...serviceSpendUsd.values()].reduce((sum, value) => sum + value, 0));
88
+ const remainingDailyBudgetUsd = roundUsd(Math.max(0, config.dailyBudgetUsd - spentThisSessionUsd));
89
+ const projectedCostIfRepeated5xUsd = roundUsd(estimateRouteCost({
90
+ route: input.route,
91
+ url: input.url,
92
+ method: input.method,
93
+ serviceId,
94
+ projectedRequests: 5,
95
+ hasReusableSession: input.hasReusableSession,
96
+ }));
97
+ const catalog = resolveCatalogEntry({
98
+ url: input.url,
99
+ method: input.method,
100
+ serviceId,
101
+ projectedRequests: input.projectedRequests,
102
+ streaming: false,
103
+ });
104
+ const repeatDecision = router.explain({
105
+ url: input.url,
106
+ method: input.method,
107
+ serviceId,
108
+ projectedRequests: Math.max(callsThisService + 1, catalog.routingHints.breakEvenCalls),
109
+ streaming: catalog.routingHints.streamingPreferred,
110
+ hasReusableSession: input.hasReusableSession,
111
+ });
112
+ let recommendation = `${repeatDecision.service?.displayName ?? serviceId} stays on ${repeatDecision.route} under the current usage forecast.`;
113
+ if (input.route === 'x402' &&
114
+ catalog.capabilities.mppSession &&
115
+ callsThisService < catalog.routingHints.breakEvenCalls) {
116
+ const remainingCalls = catalog.routingHints.breakEvenCalls - callsThisService;
117
+ recommendation = `Switch to MPP session after ${remainingCalls} more calls to amortize channel setup.`;
118
+ }
119
+ else if (input.route === 'mpp-session-open' || input.route === 'mpp-session-reuse') {
120
+ recommendation = 'Keep reusing the current MPP session to compress repeat spend into one channel lifecycle.';
121
+ }
122
+ else if (input.route === 'mpp-charge') {
123
+ recommendation = 'MPP charge remains the cleanest path for premium one-shot calls on this service.';
124
+ }
125
+ return {
126
+ agentId: agent.agentId,
127
+ agentDisplayName: agent.displayName,
128
+ agentSpentThisSessionUsd,
129
+ agentRemainingDailyBudgetUsd,
130
+ spentThisSessionUsd,
131
+ remainingDailyBudgetUsd,
132
+ callsThisService,
133
+ projectedCostIfRepeated5xUsd,
134
+ recommendation,
135
+ };
136
+ }
137
+ export function buildAgentStates(profiles = listXmppAgentProfiles()) {
138
+ return profiles.map((profile) => {
139
+ const spentThisSessionUsd = roundUsd(agentSpendUsd.get(profile.agentId) ?? 0);
140
+ return {
141
+ agentId: profile.agentId,
142
+ displayName: profile.displayName,
143
+ role: profile.role,
144
+ description: profile.description,
145
+ dailyBudgetUsd: profile.dailyBudgetUsd,
146
+ spentThisSessionUsd,
147
+ remainingDailyBudgetUsd: roundUsd(Math.max(0, profile.dailyBudgetUsd - spentThisSessionUsd)),
148
+ routeCounts: { ...getAgentRouteCounter(profile.agentId) },
149
+ allowedServices: [...profile.allowedServices],
150
+ preferredRoutes: [...profile.preferredRoutes],
151
+ enabled: profile.enabled ?? true,
152
+ policySource: profile.policySource ?? 'local',
153
+ autopayMethods: [...profile.autopayMethods],
154
+ };
155
+ });
156
+ }
157
+ export function getXmppOperatorState(agentProfiles = listXmppAgentProfiles()) {
158
+ const spentThisSessionUsd = roundUsd([...serviceSpendUsd.values()].reduce((sum, value) => sum + value, 0));
159
+ return {
160
+ sharedTreasuryUsd: config.dailyBudgetUsd,
161
+ sharedTreasuryRemainingUsd: roundUsd(Math.max(0, config.dailyBudgetUsd - spentThisSessionUsd)),
162
+ dailyBudgetUsd: config.dailyBudgetUsd,
163
+ spentThisSessionUsd,
164
+ remainingDailyBudgetUsd: roundUsd(Math.max(0, config.dailyBudgetUsd - spentThisSessionUsd)),
165
+ sessionSavingsUsd,
166
+ routeCounts: { ...routeCounts },
167
+ serviceSpendUsd: Object.fromEntries(serviceSpendUsd),
168
+ serviceCallCounts: Object.fromEntries(serviceCallCounts),
169
+ agentProfiles,
170
+ agentStates: buildAgentStates(agentProfiles),
171
+ openSessions: listLocalSessions(),
172
+ recentEvents: [...recentEvents],
173
+ };
174
+ }
175
+ export function resetXmppOperatorState() {
176
+ recentEvents.length = 0;
177
+ serviceSpendUsd.clear();
178
+ serviceCallCounts.clear();
179
+ agentSpendUsd.clear();
180
+ agentServiceCallCounts.clear();
181
+ agentRouteCounts.clear();
182
+ openSessions.clear();
183
+ sessionSavingsUsd = 0;
184
+ routeCounts.x402 = 0;
185
+ routeCounts['mpp-charge'] = 0;
186
+ routeCounts['mpp-session-open'] = 0;
187
+ routeCounts['mpp-session-reuse'] = 0;
188
+ }
@@ -0,0 +1,2 @@
1
+ import pino from 'pino';
2
+ export declare const logger: pino.Logger<never, boolean>;
@@ -0,0 +1,18 @@
1
+ import pino from 'pino';
2
+ export const logger = pino({
3
+ name: 'xmpp',
4
+ level: process.env.LOG_LEVEL ?? 'info',
5
+ redact: {
6
+ paths: [
7
+ 'req.headers.authorization',
8
+ 'req.headers.x-api-key',
9
+ 'req.headers.cookie',
10
+ 'req.body.headers.authorization',
11
+ 'req.body.headers.x-api-key',
12
+ 'req.body.options.idempotencyKey',
13
+ 'err.config.headers.authorization',
14
+ 'err.config.headers.x-api-key',
15
+ ],
16
+ censor: '[REDACTED]',
17
+ },
18
+ });
@@ -0,0 +1,27 @@
1
+ import { Keypair, xdr } from '@stellar/stellar-sdk';
2
+ import type { PaymentChallenge, PaymentExecutionMetadata, PaymentExecutionResult, RouteKind } from '@xmpp/types';
3
+ import { XLM_SAC_TESTNET } from '@stellar/mpp';
4
+ declare function createClassicSignatureScVal(keypair: Keypair, payload: Buffer): xdr.ScVal;
5
+ declare function createDelegatedSignerScVal(publicKey: string): xdr.ScVal;
6
+ declare function createDelegatedAuthPayload(publicKey: string, contextRuleIds: number[]): xdr.ScVal;
7
+ declare function buildSmartAccountSignaturePayload(entry: xdr.SorobanAuthorizationEntry, networkPassphrase: string): Buffer<ArrayBufferLike>;
8
+ declare function buildSmartAccountAuthDigest(signaturePayload: Buffer, contextRuleIds: number[]): Buffer<ArrayBufferLike>;
9
+ declare function signDelegatedSmartAccountAuth(authEntries: xdr.SorobanAuthorizationEntry[], smartAccountContractId: string, delegatedSigner: Keypair, expiration: number, networkPassphrase: string, contextRuleIds?: number[]): xdr.SorobanAuthorizationEntry[];
10
+ export declare const __smartAccountTestUtils: {
11
+ createClassicSignatureScVal: typeof createClassicSignatureScVal;
12
+ createDelegatedSignerScVal: typeof createDelegatedSignerScVal;
13
+ createDelegatedAuthPayload: typeof createDelegatedAuthPayload;
14
+ buildSmartAccountSignaturePayload: typeof buildSmartAccountSignaturePayload;
15
+ buildSmartAccountAuthDigest: typeof buildSmartAccountAuthDigest;
16
+ signDelegatedSmartAccountAuth: typeof signDelegatedSmartAccountAuth;
17
+ };
18
+ export declare function buildRetryHeaders(challenge: PaymentChallenge, route: RouteKind): {
19
+ [challenge.retryHeaderName]: string;
20
+ 'x-xmpp-route': RouteKind;
21
+ };
22
+ export declare function preparePaymentExecution(challenge: PaymentChallenge, route: RouteKind): {
23
+ headers: Record<string, string>;
24
+ metadata: PaymentExecutionMetadata;
25
+ };
26
+ export declare function executePaymentRoute(route: RouteKind, input: RequestInfo | URL, init?: RequestInit): Promise<PaymentExecutionResult>;
27
+ export { XLM_SAC_TESTNET };