aixyz 0.34.0 → 0.36.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/README.md CHANGED
@@ -137,6 +137,7 @@ That's it. Run `bun run dev` and aixyz auto-generates the server, wires up A2A +
137
137
  | [`sub-agents`](./examples/sub-agents/) | Multiple A2A endpoints from one deployment |
138
138
  | [`with-tests`](./examples/with-tests/) | Agent with test examples |
139
139
  | [`fake-llm`](./examples/fake-llm/) | Fully deterministic testing with `fake()` model |
140
+ | [`with-vercel-blob`](./examples/with-vercel-blob/) | MCP-only txt storage using Vercel Blob |
140
141
 
141
142
  ## CLI
142
143
 
package/app/index.ts CHANGED
@@ -5,7 +5,7 @@ import { type HttpMethod, type RouteHandler, type Middleware, type RouteEntry, t
5
5
  import { PaymentGateway } from "./payment/payment";
6
6
  import { getAixyzConfig } from "@aixyz/config";
7
7
  import { loadEnvConfig } from "@next/env";
8
- import { BasePlugin, type RegisterContext, type InitializeContext } from "./plugin";
8
+ import { BasePlugin, type RegisterContext, type InitializeContext, type PaymentHookContext } from "./plugin";
9
9
 
10
10
  // Load .env and .env.production files at runtime (local files are excluded at build time).
11
11
  loadEnvConfig(process.cwd());
@@ -68,6 +68,35 @@ export class AixyzApp {
68
68
  // clear registered routes for this plugin before registration, in case of hot reload or multiple registrations
69
69
  plugin.registeredRoutes.clear();
70
70
 
71
+ const paymentHooks: PaymentHookContext | undefined = this.payment
72
+ ? {
73
+ onBeforeVerify: (h) => {
74
+ this.payment!.onBeforeVerify(h);
75
+ return paymentHooks!;
76
+ },
77
+ onAfterVerify: (h) => {
78
+ this.payment!.onAfterVerify(h);
79
+ return paymentHooks!;
80
+ },
81
+ onVerifyFailure: (h) => {
82
+ this.payment!.onVerifyFailure(h);
83
+ return paymentHooks!;
84
+ },
85
+ onBeforeSettle: (h) => {
86
+ this.payment!.onBeforeSettle(h);
87
+ return paymentHooks!;
88
+ },
89
+ onAfterSettle: (h) => {
90
+ this.payment!.onAfterSettle(h);
91
+ return paymentHooks!;
92
+ },
93
+ onSettleFailure: (h) => {
94
+ this.payment!.onSettleFailure(h);
95
+ return paymentHooks!;
96
+ },
97
+ }
98
+ : undefined;
99
+
71
100
  const ctx: RegisterContext = {
72
101
  route: (method, path, handler, options) => {
73
102
  this.route(method, path, handler, options);
@@ -76,6 +105,7 @@ export class AixyzApp {
76
105
  if (entry) plugin.registeredRoutes.set(key, entry);
77
106
  },
78
107
  use: (mw) => this.use(mw),
108
+ paymentHooks,
79
109
  };
80
110
  await plugin.register?.(ctx);
81
111
  return this;
@@ -10,9 +10,54 @@ import {
10
10
  import { ExactEvmScheme } from "@x402/evm/exact/server";
11
11
  import type { AcceptsX402, AcceptsX402Multi } from "../../accepts";
12
12
  import { normalizeAcceptsX402 } from "../../accepts";
13
- import { Network, PaymentPayload, PaymentRequirements } from "@x402/core/types";
13
+ import { Network, PaymentPayload, PaymentRequirements, VerifyResponse, SettleResponse } from "@x402/core/types";
14
14
  import { AixyzConfig } from "@aixyz/config";
15
15
 
16
+ // ── x402 Lifecycle Hook Types ──────────────────────────────────────────
17
+ // These mirror x402ResourceServer's hook signatures but are defined here
18
+ // because x402/core does not export them publicly.
19
+
20
+ export interface VerifyContext {
21
+ paymentPayload: PaymentPayload;
22
+ requirements: PaymentRequirements;
23
+ }
24
+
25
+ export interface VerifyResultContext extends VerifyContext {
26
+ result: VerifyResponse;
27
+ }
28
+
29
+ export interface VerifyFailureContext extends VerifyContext {
30
+ error: Error;
31
+ }
32
+
33
+ export interface SettleContext {
34
+ paymentPayload: PaymentPayload;
35
+ requirements: PaymentRequirements;
36
+ }
37
+
38
+ export interface SettleResultContext extends SettleContext {
39
+ result: SettleResponse;
40
+ }
41
+
42
+ export interface SettleFailureContext extends SettleContext {
43
+ error: Error;
44
+ }
45
+
46
+ export type BeforeVerifyHook = (
47
+ context: VerifyContext,
48
+ ) => Promise<void | { abort: true; reason: string; message?: string }>;
49
+ export type AfterVerifyHook = (context: VerifyResultContext) => Promise<void>;
50
+ export type OnVerifyFailureHook = (
51
+ context: VerifyFailureContext,
52
+ ) => Promise<void | { recovered: true; result: VerifyResponse }>;
53
+ export type BeforeSettleHook = (
54
+ context: SettleContext,
55
+ ) => Promise<void | { abort: true; reason: string; message?: string }>;
56
+ export type AfterSettleHook = (context: SettleResultContext) => Promise<void>;
57
+ export type OnSettleFailureHook = (
58
+ context: SettleFailureContext,
59
+ ) => Promise<void | { recovered: true; result: SettleResponse }>;
60
+
16
61
  /**
17
62
  * Converts a web-standard Request into the x402 HTTPAdapter interface.
18
63
  */
@@ -45,10 +90,12 @@ function toResponse(instructions: HTTPResponseInstructions): Response {
45
90
  return new Response(body, { status: instructions.status, headers });
46
91
  }
47
92
 
48
- interface PaymentContext {
93
+ export interface PaymentContext {
49
94
  paymentPayload: PaymentPayload;
50
95
  paymentRequirements: PaymentRequirements;
51
96
  declaredExtensions?: Record<string, unknown>;
97
+ /** The payer's address, captured from x402 verification. */
98
+ payer?: string;
52
99
  }
53
100
 
54
101
  /**
@@ -60,10 +107,22 @@ export class PaymentGateway {
60
107
  private readonly config: AixyzConfig;
61
108
  private readonly pendingRoutes = new Map<string, RouteConfig>();
62
109
  private readonly verifiedPayments = new WeakMap<Request, PaymentContext>();
110
+ /**
111
+ * Temporary map to capture payer address from the afterVerify hook.
112
+ * Keyed by PaymentPayload reference (same object flows through the verify pipeline).
113
+ */
114
+ private readonly pendingPayers = new WeakMap<PaymentPayload, string>();
63
115
 
64
116
  constructor(facilitators: FacilitatorClient | FacilitatorClient[], config: AixyzConfig) {
65
117
  this.resourceServer = new x402ResourceServer(facilitators);
66
118
  this.config = config;
119
+
120
+ // Capture payer address from verification so it can be exposed to handlers.
121
+ this.onAfterVerify(async (context) => {
122
+ if (context.result.payer) {
123
+ this.pendingPayers.set(context.paymentPayload, context.result.payer);
124
+ }
125
+ });
67
126
  }
68
127
 
69
128
  /** Register an EVM payment scheme for the given network (e.g. Base mainnet). */
@@ -145,13 +204,16 @@ export class PaymentGateway {
145
204
  case "no-payment-required":
146
205
  return null;
147
206
 
148
- case "payment-verified":
207
+ case "payment-verified": {
208
+ const payer = this.pendingPayers.get(result.paymentPayload);
149
209
  this.verifiedPayments.set(request, {
150
210
  paymentPayload: result.paymentPayload,
151
211
  paymentRequirements: result.paymentRequirements,
152
212
  declaredExtensions: result.declaredExtensions,
213
+ payer,
153
214
  });
154
215
  return null;
216
+ }
155
217
 
156
218
  case "payment-error":
157
219
  return toResponse(result.response);
@@ -175,4 +237,58 @@ export class PaymentGateway {
175
237
 
176
238
  return result;
177
239
  }
240
+
241
+ /**
242
+ * Get the payer's address for a verified payment request.
243
+ * Available after `verify()` succeeds and before `settle()` is called.
244
+ */
245
+ getPayer(request: Request): string | undefined {
246
+ return this.verifiedPayments.get(request)?.payer;
247
+ }
248
+
249
+ /**
250
+ * Get the full payment context for a verified payment request.
251
+ * Available after `verify()` succeeds and before `settle()` is called.
252
+ */
253
+ getPaymentContext(request: Request): Readonly<PaymentContext> | undefined {
254
+ return this.verifiedPayments.get(request);
255
+ }
256
+
257
+ // ── x402 Lifecycle Hooks ───────────────────────────────────────────
258
+
259
+ /** Register a hook to run before payment verification. Return `{ abort: true, reason }` to reject. */
260
+ onBeforeVerify(hook: BeforeVerifyHook): this {
261
+ this.resourceServer.onBeforeVerify(hook);
262
+ return this;
263
+ }
264
+
265
+ /** Register a hook to run after successful payment verification. */
266
+ onAfterVerify(hook: AfterVerifyHook): this {
267
+ this.resourceServer.onAfterVerify(hook);
268
+ return this;
269
+ }
270
+
271
+ /** Register a hook to run when payment verification fails. Return `{ recovered: true, result }` to recover. */
272
+ onVerifyFailure(hook: OnVerifyFailureHook): this {
273
+ this.resourceServer.onVerifyFailure(hook);
274
+ return this;
275
+ }
276
+
277
+ /** Register a hook to run before payment settlement. Return `{ abort: true, reason }` to reject. */
278
+ onBeforeSettle(hook: BeforeSettleHook): this {
279
+ this.resourceServer.onBeforeSettle(hook);
280
+ return this;
281
+ }
282
+
283
+ /** Register a hook to run after successful payment settlement. */
284
+ onAfterSettle(hook: AfterSettleHook): this {
285
+ this.resourceServer.onAfterSettle(hook);
286
+ return this;
287
+ }
288
+
289
+ /** Register a hook to run when payment settlement fails. Return `{ recovered: true, result }` to recover. */
290
+ onSettleFailure(hook: OnSettleFailureHook): this {
291
+ this.resourceServer.onSettleFailure(hook);
292
+ return this;
293
+ }
178
294
  }
package/app/plugin.ts CHANGED
@@ -1,5 +1,26 @@
1
1
  import type { HttpMethod, RouteHandler, RouteEntry, RouteOptions, Middleware } from "./types";
2
- import type { PaymentGateway } from "./payment/payment";
2
+ import type {
3
+ PaymentGateway,
4
+ BeforeVerifyHook,
5
+ AfterVerifyHook,
6
+ OnVerifyFailureHook,
7
+ BeforeSettleHook,
8
+ AfterSettleHook,
9
+ OnSettleFailureHook,
10
+ } from "./payment/payment";
11
+
12
+ /**
13
+ * Scoped interface for registering x402 payment lifecycle hooks.
14
+ * Exposes only hook registration — not the full PaymentGateway.
15
+ */
16
+ export interface PaymentHookContext {
17
+ onBeforeVerify(hook: BeforeVerifyHook): PaymentHookContext;
18
+ onAfterVerify(hook: AfterVerifyHook): PaymentHookContext;
19
+ onVerifyFailure(hook: OnVerifyFailureHook): PaymentHookContext;
20
+ onBeforeSettle(hook: BeforeSettleHook): PaymentHookContext;
21
+ onAfterSettle(hook: AfterSettleHook): PaymentHookContext;
22
+ onSettleFailure(hook: OnSettleFailureHook): PaymentHookContext;
23
+ }
3
24
 
4
25
  /**
5
26
  * Scoped context passed to {@link BasePlugin.register}.
@@ -16,6 +37,8 @@ export interface RegisterContext {
16
37
  route(method: HttpMethod, path: string, handler: RouteHandler, options?: RouteOptions): void;
17
38
  /** Append a middleware to the application's middleware chain. */
18
39
  use(middleware: Middleware): void;
40
+ /** Register hooks on the x402 payment lifecycle. Undefined when no facilitators configured. */
41
+ readonly paymentHooks?: PaymentHookContext;
19
42
  }
20
43
 
21
44
  /**
@@ -25,7 +25,8 @@ export type Capabilities = z.infer<typeof CapabilitiesSchema>;
25
25
 
26
26
  export interface A2AAgentEntry {
27
27
  name?: string;
28
- exports: { default: ToolLoopAgent; accepts?: Accepts; capabilities?: Capabilities };
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ exports: { default: ToolLoopAgent<never, any>; accepts?: Accepts; capabilities?: Capabilities };
29
30
  /** Optional task store override. Defaults to a per-agent InMemoryTaskStore for isolation. */
30
31
  taskStore?: TaskStore;
31
32
  }
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { type Tool } from "ai";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -8,6 +9,13 @@ import { AcceptsScheme, isAcceptsPaid, normalizeAcceptsX402 } from "../../accept
8
9
  import { getAixyzConfig, getAixyzConfigRuntime } from "../../config";
9
10
  import { Network } from "@x402/core/types";
10
11
  import { ExactEvmScheme } from "@x402/evm/exact/server";
12
+ import type { SessionPlugin } from "./session/index";
13
+
14
+ /**
15
+ * AsyncLocalStorage to pass the MCP-level payer address from the
16
+ * onAfterVerify hook to the tool handler within the same async context.
17
+ */
18
+ const mcpPayerStorage = new AsyncLocalStorage<{ payer?: string }>();
11
19
 
12
20
  /**
13
21
  * MCP (Model Context Protocol) plugin. Collects tools and exposes them
@@ -21,6 +29,7 @@ export class MCPPlugin extends BasePlugin {
21
29
  readonly name = "mcp";
22
30
  readonly registeredTools: Array<{ name: string; tool: Tool; accepts?: Accepts }> = [];
23
31
  private paymentWrappers = new Map<string, (handler: any) => any>();
32
+ private sessionPlugin?: SessionPlugin;
24
33
 
25
34
  constructor(private tools: Array<{ name: string; exports: { default: Tool; accepts?: Accepts } }>) {
26
35
  super();
@@ -31,22 +40,44 @@ export class MCPPlugin extends BasePlugin {
31
40
  const mcpServer = new McpServer({ name: config.name, version: config.version }, { capabilities: { tools: {} } });
32
41
 
33
42
  for (const { name, tool } of this.registeredTools) {
43
+ const sessionPlugin = this.sessionPlugin;
34
44
  const handler = async (args: Record<string, unknown>) => {
35
- try {
36
- const result = await tool.execute!(args, { toolCallId: name, messages: [] });
37
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
38
- return { content: [{ type: "text" as const, text }] };
39
- } catch (error) {
40
- const text = error instanceof Error ? error.message : "Unknown error";
41
- return { content: [{ type: "text" as const, text: `Error: ${text}` }], isError: true };
45
+ const execute = async () => {
46
+ try {
47
+ const result = await tool.execute!(args, { toolCallId: name, messages: [] });
48
+ const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
49
+ return { content: [{ type: "text" as const, text }] };
50
+ } catch (error) {
51
+ console.error(`[mcp] Tool "${name}" failed:`, error);
52
+ const text = error instanceof Error ? error.message : "Unknown error";
53
+ return { content: [{ type: "text" as const, text: `Error: ${text}` }], isError: true };
54
+ }
55
+ };
56
+
57
+ // If a payer was captured from MCP-level payment, run the tool
58
+ // within a session context so getSession() works.
59
+ const payer = mcpPayerStorage.getStore()?.payer;
60
+ if (payer && sessionPlugin) {
61
+ return sessionPlugin.runWithPayer(payer, execute);
42
62
  }
63
+ return execute();
43
64
  };
44
65
 
45
66
  const wrapper = this.paymentWrappers.get(name);
67
+ let registeredHandler: (args: any, extra?: any) => any;
68
+ if (wrapper) {
69
+ const wrapped = wrapper(handler);
70
+ // Wrap the payment wrapper call in mcpPayerStorage.run() so the
71
+ // onAfterVerify hook and handler share the same async context.
72
+ registeredHandler = (args: any, extra: any) =>
73
+ mcpPayerStorage.run({ payer: undefined }, () => wrapped(args, extra));
74
+ } else {
75
+ registeredHandler = handler;
76
+ }
46
77
  mcpServer.registerTool(
47
78
  name,
48
79
  { description: tool.description, inputSchema: tool.inputSchema as any },
49
- wrapper ? wrapper(handler) : handler,
80
+ registeredHandler,
50
81
  );
51
82
  }
52
83
 
@@ -80,6 +111,8 @@ export class MCPPlugin extends BasePlugin {
80
111
  }
81
112
 
82
113
  async initialize(ctx: InitializeContext): Promise<void> {
114
+ this.sessionPlugin = ctx.getPlugin<SessionPlugin>("session");
115
+
83
116
  if (!ctx.payment) return;
84
117
 
85
118
  const config = getAixyzConfig();
@@ -102,6 +135,17 @@ export class MCPPlugin extends BasePlugin {
102
135
  }
103
136
  }
104
137
 
138
+ // Capture payer from MCP-level payment verification for session integration.
139
+ // This hook is global (fires for all verifications, including HTTP-level), but
140
+ // only writes when mcpPayerStorage has an active context — which only happens
141
+ // inside MCP tool calls wrapped by mcpPayerStorage.run() below.
142
+ ctx.payment.onAfterVerify(async (context) => {
143
+ const store = mcpPayerStorage.getStore();
144
+ if (store && context.result.payer) {
145
+ store.payer = context.result.payer;
146
+ }
147
+ });
148
+
105
149
  for (const { name, accepts } of this.registeredTools) {
106
150
  if (!accepts || !isAcceptsPaid(accepts)) continue;
107
151
 
@@ -0,0 +1,193 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { BasePlugin, type RegisterContext, type InitializeContext } from "../../plugin";
3
+ import type { PaymentGateway } from "../../payment/payment";
4
+ import { InMemorySessionStore } from "./memory";
5
+
6
+ export { InMemorySessionStore, type InMemorySessionStoreOptions } from "./memory";
7
+
8
+ // ── Storage Interface ────────────────────────────────────────────────
9
+
10
+ /** Options for {@link SessionStore.set} and {@link SessionStore.setMany}. */
11
+ export interface SetOptions {
12
+ /** Per-key TTL in milliseconds. Overrides the store-level default when supported. */
13
+ ttlMs?: number;
14
+ }
15
+
16
+ /** Options for {@link SessionStore.list}. */
17
+ export interface ListOptions {
18
+ /** Only return keys starting with this prefix. */
19
+ prefix?: string;
20
+ /** Opaque cursor from a previous `list()` call for pagination. */
21
+ cursor?: string;
22
+ /** Maximum number of entries to return. The store decides its own default when omitted. */
23
+ limit?: number;
24
+ /** If true, values are omitted (all values will be empty strings). */
25
+ keysOnly?: boolean;
26
+ }
27
+
28
+ /** Return type for {@link SessionStore.list}. */
29
+ export interface ListResult {
30
+ entries: Record<string, string>;
31
+ /** If present, more results are available — pass to the next `list()` call. */
32
+ cursor?: string;
33
+ }
34
+
35
+ /**
36
+ * Pluggable session storage backend.
37
+ * All methods are async to support external stores (Redis, DB, KV, etc.).
38
+ * Operations are scoped by payer address — the x402 signer for the request.
39
+ */
40
+ export interface SessionStore {
41
+ get(payer: string, key: string): Promise<string | undefined>;
42
+ set(payer: string, key: string, value: string, options?: SetOptions): Promise<void>;
43
+ delete(payer: string, key: string): Promise<boolean>;
44
+ list(payer: string, options?: ListOptions): Promise<ListResult>;
45
+
46
+ /** Retrieve multiple keys in a single call. */
47
+ getMany?(payer: string, keys: string[]): Promise<Record<string, string | undefined>>;
48
+ /** Set multiple keys in a single call. */
49
+ setMany?(payer: string, entries: Record<string, string>, options?: SetOptions): Promise<void>;
50
+ /** Delete multiple keys in a single call. Returns the number of keys actually removed. */
51
+ deleteMany?(payer: string, keys: string[]): Promise<number>;
52
+
53
+ /** Release resources held by the store (connections, timers, etc.). */
54
+ close?(): Promise<void>;
55
+ }
56
+
57
+ // ── Session Handle ───────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Payer-scoped session handle. All operations target the current
61
+ * x402 payer without requiring the caller to pass an address.
62
+ */
63
+ export interface Session {
64
+ readonly payer: string;
65
+ get(key: string): Promise<string | undefined>;
66
+ set(key: string, value: string, options?: SetOptions): Promise<void>;
67
+ delete(key: string): Promise<boolean>;
68
+ list(options?: ListOptions): Promise<ListResult>;
69
+ }
70
+
71
+ function createSession(payer: string, store: SessionStore): Session {
72
+ // Normalize the payer address to lowercase so sessions are keyed consistently
73
+ // regardless of whether the x402 verifier returns a checksummed (EIP-55) or
74
+ // lowercase address. The original (possibly checksummed) address is still
75
+ // exposed as `session.payer` for display purposes.
76
+ const storedPayer = payer.toLowerCase();
77
+ return {
78
+ payer,
79
+ get: (key) => store.get(storedPayer, key),
80
+ set: (key, value, options) => store.set(storedPayer, key, value, options),
81
+ delete: (key) => store.delete(storedPayer, key),
82
+ list: (options) => store.list(storedPayer, options),
83
+ };
84
+ }
85
+
86
+ // ── AsyncLocalStorage API ────────────────────────────────────────────
87
+
88
+ const sessionStorage = new AsyncLocalStorage<Session | undefined>();
89
+
90
+ /**
91
+ * Get the current payer-scoped session.
92
+ * Returns `undefined` if no x402 payment was made for this request.
93
+ */
94
+ export function getSession(): Session | undefined {
95
+ return sessionStorage.getStore();
96
+ }
97
+
98
+ /**
99
+ * Get the current x402 payer address.
100
+ * Shorthand for `getSession()?.payer`.
101
+ */
102
+ export function getPayer(): string | undefined {
103
+ return sessionStorage.getStore()?.payer;
104
+ }
105
+
106
+ // ── Plugin ───────────────────────────────────────────────────────────
107
+
108
+ export interface SessionPluginOptions {
109
+ /** Custom session store. Defaults to {@link InMemorySessionStore}. */
110
+ store?: SessionStore;
111
+ }
112
+
113
+ /**
114
+ * Session plugin for aixyz. Provides payer-scoped key-value storage
115
+ * gated by x402 payment identity.
116
+ *
117
+ * Register **before** other plugins so the session middleware runs first:
118
+ *
119
+ * ```ts
120
+ * await app.withPlugin(new SessionPlugin());
121
+ * await app.withPlugin(new A2APlugin([...]));
122
+ * ```
123
+ *
124
+ * Tools access the session via {@link getSession}:
125
+ *
126
+ * ```ts
127
+ * import { getSession } from "aixyz/app/plugins/session";
128
+ *
129
+ * const session = getSession();
130
+ * await session?.set("key", "value");
131
+ * ```
132
+ */
133
+ export class SessionPlugin extends BasePlugin {
134
+ readonly name = "session";
135
+ readonly store: SessionStore;
136
+ private payment?: PaymentGateway;
137
+
138
+ constructor(options?: SessionPluginOptions) {
139
+ super();
140
+ this.store = options?.store ?? new InMemorySessionStore();
141
+ }
142
+
143
+ register(ctx: RegisterContext): void {
144
+ ctx.use(async (request, next) => {
145
+ const payer = this.payment?.getPayer(request);
146
+ if (payer) {
147
+ const session = createSession(payer, this.store);
148
+ return sessionStorage.run(session, next);
149
+ }
150
+ // Explicitly clear any inherited ALS context so that an unpaid route
151
+ // nested inside a paid handler's app.fetch() doesn't leak the outer session.
152
+ return sessionStorage.run(undefined, next);
153
+ });
154
+ }
155
+
156
+ initialize(ctx: InitializeContext): void {
157
+ this.payment = ctx.payment;
158
+ }
159
+
160
+ /**
161
+ * Run a function within a session context for the given payer.
162
+ * Used by other plugins (e.g., MCPPlugin) to set session context
163
+ * for tool execution when payment is handled at the protocol level.
164
+ *
165
+ * **Security note:** This method bypasses HTTP-level payment verification.
166
+ * Only call it with a payer address that has already been authenticated by a
167
+ * trusted payment mechanism (e.g., the `@x402/mcp` payment wrapper).
168
+ * @internal
169
+ */
170
+ runWithPayer<T>(payer: string, fn: () => T): T {
171
+ const session = createSession(payer, this.store);
172
+ return sessionStorage.run(session, fn);
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Identity helper that provides full type inference for a {@link SessionStore}.
178
+ * Use in `app/session.ts` to get type-safe autocompletion:
179
+ *
180
+ * ```ts
181
+ * import { defineSessionStore } from "aixyz/app/plugins/session";
182
+ *
183
+ * export default defineSessionStore({
184
+ * async get(payer, key) { ... },
185
+ * async set(payer, key, value) { ... },
186
+ * async delete(payer, key) { ... },
187
+ * async list(payer) { ... },
188
+ * });
189
+ * ```
190
+ */
191
+ export function defineSessionStore(store: SessionStore): SessionStore {
192
+ return store;
193
+ }