@apiosk/mcp 1.7.0 → 2.0.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 (100) hide show
  1. package/README.md +83 -612
  2. package/assets/brand/apiosk-a-20260921.png +0 -0
  3. package/assets/brand/apple-touch-icon.png +0 -0
  4. package/assets/brand/favicon-dark.ico +0 -0
  5. package/assets/brand/favicon-light.ico +0 -0
  6. package/assets/brand/favicon.ico +0 -0
  7. package/assets/brand/icon-192.png +0 -0
  8. package/assets/brand/icon-512.png +0 -0
  9. package/assets/brand/icon-maskable-512.png +0 -0
  10. package/assets/brand/inter-OFL.txt +93 -0
  11. package/assets/brand/inter-latin-400-normal.woff2 +0 -0
  12. package/assets/brand/inter-latin-500-normal.woff2 +0 -0
  13. package/assets/brand/inter-latin-600-normal.woff2 +0 -0
  14. package/assets/brand/mark-20260905-transparent.svg +1 -0
  15. package/assets/brand/mark-20260918.svg +1 -0
  16. package/assets/brand/mark-black-20260918.svg +1 -0
  17. package/assets/brand/mark-dark-20260905-transparent.png +0 -0
  18. package/assets/brand/mark-dark-20260918.png +0 -0
  19. package/assets/brand/mark-dark-96.png +0 -0
  20. package/assets/brand/mark-light-20260905-transparent.png +0 -0
  21. package/assets/brand/mark-light-20260918.png +0 -0
  22. package/assets/brand/mark-light-96.png +0 -0
  23. package/assets/brand/wordmark-black-320.png +0 -0
  24. package/assets/brand/wordmark-white-320.png +0 -0
  25. package/docs/branding.md +11 -0
  26. package/docs/marketplace-submission-2026-04-08.md +4 -0
  27. package/docs/marketplace-submission-2026-08-20.md +157 -0
  28. package/docs/openai-plugin-submission-2026-09-05.md +143 -0
  29. package/dxt.json +26 -22
  30. package/index.mjs +3 -1
  31. package/logo-optimized-light.png +0 -0
  32. package/package.json +14 -14
  33. package/plugin/apiosk/.codex-plugin/plugin.json +45 -0
  34. package/plugin/apiosk/.mcp.json +8 -0
  35. package/plugin/apiosk/assets/icon-dark.png +0 -0
  36. package/plugin/apiosk/assets/icon.png +0 -0
  37. package/plugin/apiosk/assets/icon.svg +1 -0
  38. package/plugin/apiosk/assets/logo.png +0 -0
  39. package/plugin/apiosk/skills/apiosk/SKILL.md +33 -0
  40. package/plugin/apiosk/skills/apiosk/agents/openai.yaml +12 -0
  41. package/server.json +58 -24
  42. package/server.mjs +136 -151
  43. package/src/approval-feedback.mjs +21 -0
  44. package/src/brand-routes.mjs +28 -0
  45. package/src/create-server.mjs +160 -46
  46. package/src/display-money.mjs +20 -0
  47. package/src/display-text.mjs +67 -0
  48. package/src/gateway-client.mjs +42 -0
  49. package/src/gateway-v2-ask.mjs +50 -0
  50. package/src/gateway-v2-card-account.mjs +23 -0
  51. package/src/gateway-v2-card-actions.mjs +67 -0
  52. package/src/gateway-v2-card-answer-text.mjs +130 -0
  53. package/src/gateway-v2-card-answer.mjs +68 -0
  54. package/src/gateway-v2-card-blocks.mjs +166 -0
  55. package/src/gateway-v2-card-body.mjs +259 -0
  56. package/src/gateway-v2-card-budget.mjs +21 -0
  57. package/src/gateway-v2-card-cbs.mjs +56 -0
  58. package/src/gateway-v2-card-choices.mjs +4 -0
  59. package/src/gateway-v2-card-clarification.mjs +25 -0
  60. package/src/gateway-v2-card-compact.mjs +34 -0
  61. package/src/gateway-v2-card-events.mjs +41 -0
  62. package/src/gateway-v2-card-presentation.mjs +125 -0
  63. package/src/gateway-v2-card-research.mjs +63 -0
  64. package/src/gateway-v2-card-result.mjs +32 -0
  65. package/src/gateway-v2-card-search.mjs +39 -0
  66. package/src/gateway-v2-card-sources.mjs +32 -0
  67. package/src/gateway-v2-card-style.mjs +76 -0
  68. package/src/gateway-v2-card-verdict.mjs +66 -0
  69. package/src/gateway-v2-card.mjs +91 -0
  70. package/src/gateway-v2-contracts.json +66 -0
  71. package/src/gateway-v2-instructions.md +113 -0
  72. package/src/gateway-v2-recovery.mjs +18 -0
  73. package/src/gateway-v2-report-links.mjs +14 -0
  74. package/src/gateway-v2-workflows.mjs +17 -0
  75. package/src/gateway-v2.mjs +179 -0
  76. package/src/oauth.mjs +402 -1949
  77. package/src/observability.mjs +19 -3
  78. package/src/result-presentation.mjs +9 -0
  79. package/src/runtime.mjs +24 -3670
  80. package/src/source-groups.mjs +91 -0
  81. package/src/source-value-format.mjs +25 -0
  82. package/src/tool-result.mjs +20 -0
  83. package/src/ui-bridge.mjs +185 -0
  84. package/src/well-known-routes.mjs +132 -0
  85. package/src/assets/wallet-accounts.mjs +0 -20513
  86. package/src/assets/walletconnect-provider.mjs +0 -6319
  87. package/src/discovery.mjs +0 -929
  88. package/src/external-fetch.mjs +0 -203
  89. package/src/funding-options.mjs +0 -255
  90. package/src/gateway-management.mjs +0 -107
  91. package/src/hosted-payment.mjs +0 -552
  92. package/src/hosted-wallets.mjs +0 -530
  93. package/src/listing-metadata.mjs +0 -287
  94. package/src/local-config.mjs +0 -256
  95. package/src/payment-guidance.mjs +0 -298
  96. package/src/publisher.mjs +0 -1288
  97. package/src/result-canvas.mjs +0 -16
  98. package/src/source-registry.mjs +0 -215
  99. package/src/wallet-store.mjs +0 -476
  100. package/src/x402-inspect.mjs +0 -361
package/src/publisher.mjs DELETED
@@ -1,1288 +0,0 @@
1
- // Apiosk Publisher MCP tools.
2
- //
3
- // Spec-shaped tool surface ("publish this API as a paid x402 endpoint") for
4
- // coding agents: publish_x402_route, list_x402_routes, update_x402_route,
5
- // unpublish_x402_route, test_x402_route, generate_openapi_spec,
6
- // publish_project.
7
- //
8
- // Identity model: these tools authenticate with a provider API key
9
- // (sk_live_…, minted in the provider portal) passed as
10
- // `Authorization: Bearer sk_live_…`. The key is verified through the
11
- // gateway database's verify_provider_api_key() RPC and resolves to the
12
- // provider's owner_id, the same identity the provider portal writes with.
13
- // Routes therefore show up in the portal, the gateway /.well-known/x402
14
- // document, and the Bazaar indexing pipeline like any portal-published API.
15
- //
16
- // A "route" is one paid endpoint: an `api_endpoints` row under an `apis`
17
- // listing. route_id == api_endpoints.id.
18
-
19
- const DEFAULT_GATEWAY_BASE_URL = "https://gateway.apiosk.com";
20
- const DEFAULT_MCP_PUBLIC_BASE_URL = "https://mcp.apiosk.com";
21
- // The Supabase project URL is public client config (the portals ship it in
22
- // their bundles); only the service-role key is secret and must come from env.
23
- const DEFAULT_SUPABASE_URL = "https://jgjoiyqdyypouskftzeq.supabase.co";
24
-
25
- const ALLOWED_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
26
- const ALLOWED_NETWORKS = ["base", "polygon", "arbitrum", "solana"];
27
- const PROVIDER_KEY_PREFIX = "sk_live_";
28
- const PROVIDER_KEY_CACHE_TTL_MS = 60_000;
29
- const DISCOVERY_CACHE_TTL_MS = 60_000;
30
- const MAX_PRICE_USDC = 1000;
31
-
32
- const providerKeyCache = new Map();
33
- let discoveryCache = null;
34
-
35
- function trimString(value) {
36
- return String(value ?? "").trim();
37
- }
38
-
39
- function content(value) {
40
- const result = {
41
- content: [
42
- {
43
- type: "text",
44
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
45
- },
46
- ],
47
- };
48
-
49
- if (value && typeof value === "object" && !Array.isArray(value)) {
50
- result.structuredContent = value;
51
- }
52
-
53
- return result;
54
- }
55
-
56
- function errorContent(value) {
57
- return {
58
- content: [
59
- {
60
- type: "text",
61
- text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
62
- },
63
- ],
64
- isError: true,
65
- };
66
- }
67
-
68
- export const PUBLISHER_TOOLS = [
69
- {
70
- name: "publish_x402_route",
71
- title: "Publish a paid x402 route",
72
- annotations: {
73
- title: "Publish a paid x402 route",
74
- readOnlyHint: false,
75
- destructiveHint: false,
76
- idempotentHint: true,
77
- openWorldHint: true,
78
- },
79
- description:
80
- "Publish an API endpoint as a paid x402 route on the Apiosk gateway. The route gets a paid URL that returns 402 Payment Required until the caller pays in USDC, then forwards to your upstream API. New routes enter Apiosk's review queue (status pending_review) and go live on approval. Requires an Apiosk provider token (Authorization: Bearer sk_live_…).",
81
- inputSchema: {
82
- type: "object",
83
- required: ["name", "upstream_url", "price", "settlement_address"],
84
- properties: {
85
- name: { type: "string", description: "Human-readable API name, e.g. 'Weather API'." },
86
- description: { type: "string" },
87
- upstream_url: {
88
- type: "string",
89
- description: "Full HTTPS URL of your existing endpoint the gateway forwards paid requests to.",
90
- },
91
- method: { type: "string", enum: ALLOWED_METHODS, description: "Defaults to GET." },
92
- path: {
93
- type: "string",
94
- description: "Public path under the route's gateway slug, e.g. /weather. Defaults to the last segment of upstream_url.",
95
- },
96
- price: {
97
- type: ["string", "number"],
98
- description: "Price per call in USDC, e.g. \"0.01\".",
99
- },
100
- currency: { type: "string", enum: ["USDC"], description: "Only USDC is supported." },
101
- network: {
102
- type: "string",
103
- enum: ALLOWED_NETWORKS,
104
- description: "Settlement network. Defaults to base.",
105
- },
106
- settlement_address: {
107
- type: "string",
108
- description: "Wallet that receives 98% of each payment (Apiosk keeps a 2% platform fee).",
109
- },
110
- input_schema: { type: "object", additionalProperties: true },
111
- output_schema: { type: "object", additionalProperties: true },
112
- tags: { type: "array", items: { type: "string" } },
113
- slug: {
114
- type: "string",
115
- description: "Optional gateway slug override (lowercase letters, numbers, hyphens). Defaults to a slug derived from name.",
116
- },
117
- },
118
- },
119
- },
120
- {
121
- name: "update_x402_route",
122
- title: "Update a paid x402 route",
123
- annotations: {
124
- title: "Update a paid x402 route",
125
- readOnlyHint: false,
126
- destructiveHint: false,
127
- idempotentHint: true,
128
- openWorldHint: true,
129
- },
130
- description:
131
- "Update a published x402 route: price, description, upstream URL, schemas, settlement address, or status. Requires an Apiosk provider token.",
132
- inputSchema: {
133
- type: "object",
134
- required: ["route_id"],
135
- properties: {
136
- route_id: { type: "string" },
137
- name: { type: "string" },
138
- description: { type: "string" },
139
- upstream_url: { type: "string" },
140
- method: { type: "string", enum: ALLOWED_METHODS },
141
- path: { type: "string" },
142
- price: { type: ["string", "number"] },
143
- settlement_address: { type: "string" },
144
- input_schema: { type: "object", additionalProperties: true },
145
- output_schema: { type: "object", additionalProperties: true },
146
- tags: { type: "array", items: { type: "string" } },
147
- status: {
148
- type: "string",
149
- enum: ["active", "disabled"],
150
- description: "Re-enable ('active', re-enters review if not yet approved) or disable the route's listing.",
151
- },
152
- },
153
- },
154
- },
155
- {
156
- name: "list_x402_routes",
157
- title: "List my paid x402 routes",
158
- annotations: {
159
- title: "List my paid x402 routes",
160
- readOnlyHint: true,
161
- openWorldHint: true,
162
- },
163
- description:
164
- "List all x402 routes published by the authenticated provider, with paid URLs, prices, and live status. Requires an Apiosk provider token.",
165
- inputSchema: {
166
- type: "object",
167
- additionalProperties: false,
168
- properties: {
169
- status: {
170
- type: "string",
171
- enum: ["active", "pending_review", "disabled"],
172
- description: "Optional status filter.",
173
- },
174
- },
175
- },
176
- },
177
- {
178
- name: "unpublish_x402_route",
179
- title: "Unpublish (disable) a paid x402 route",
180
- annotations: {
181
- title: "Unpublish (disable) a paid x402 route",
182
- readOnlyHint: false,
183
- destructiveHint: true,
184
- idempotentHint: true,
185
- openWorldHint: true,
186
- },
187
- description:
188
- "Disable a paid x402 route (its gateway listing stops serving). Reversible with update_x402_route {status: 'active'}. Requires an Apiosk provider token.",
189
- inputSchema: {
190
- type: "object",
191
- required: ["route_id"],
192
- properties: {
193
- route_id: { type: "string" },
194
- },
195
- },
196
- },
197
- {
198
- name: "test_x402_route",
199
- title: "Test a route's x402 payment behavior",
200
- annotations: {
201
- title: "Test a route's x402 payment behavior",
202
- readOnlyHint: true,
203
- openWorldHint: true,
204
- },
205
- description:
206
- "Send an unpaid test request to a route's paid URL and verify x402 behavior: expects 402 Payment Required with a valid accepts[] payment offer. Requires an Apiosk provider token.",
207
- inputSchema: {
208
- type: "object",
209
- required: ["route_id"],
210
- properties: {
211
- route_id: { type: "string" },
212
- test_payload: {
213
- type: "object",
214
- additionalProperties: true,
215
- description: "Sent as query params for GET, JSON body otherwise.",
216
- },
217
- },
218
- },
219
- },
220
- {
221
- name: "generate_openapi_spec",
222
- title: "Generate a hosted OpenAPI spec",
223
- annotations: {
224
- title: "Generate a hosted OpenAPI spec",
225
- readOnlyHint: false,
226
- destructiveHint: false,
227
- idempotentHint: true,
228
- openWorldHint: true,
229
- },
230
- description:
231
- "Generate a hosted OpenAPI 3.1 spec for a route's API listing (all its endpoints) and return its public URL. Requires an Apiosk provider token.",
232
- inputSchema: {
233
- type: "object",
234
- required: ["route_id"],
235
- properties: {
236
- route_id: { type: "string" },
237
- title: { type: "string" },
238
- version: { type: "string" },
239
- endpoints: {
240
- type: "array",
241
- items: { type: "object", additionalProperties: true },
242
- description: "Optional extra path items merged into the generated spec.",
243
- },
244
- },
245
- },
246
- },
247
- {
248
- name: "publish_project",
249
- title: "Publish a whole project's routes",
250
- annotations: {
251
- title: "Publish a whole project's routes",
252
- readOnlyHint: false,
253
- destructiveHint: false,
254
- idempotentHint: true,
255
- openWorldHint: true,
256
- },
257
- description:
258
- "Publish multiple API routes from one project in a single call. Creates one gateway listing for the project and one paid x402 route per entry. Requires an Apiosk provider token.",
259
- inputSchema: {
260
- type: "object",
261
- required: ["project_name", "base_url", "settlement_address", "routes"],
262
- properties: {
263
- project_name: { type: "string" },
264
- description: { type: "string" },
265
- base_url: {
266
- type: "string",
267
- description: "HTTPS base URL of the project's API. Each route's path is appended to it upstream.",
268
- },
269
- settlement_address: { type: "string" },
270
- network: { type: "string", enum: ALLOWED_NETWORKS },
271
- tags: { type: "array", items: { type: "string" } },
272
- routes: {
273
- type: "array",
274
- minItems: 1,
275
- items: {
276
- type: "object",
277
- required: ["name", "path", "price"],
278
- properties: {
279
- name: { type: "string" },
280
- path: { type: "string" },
281
- method: { type: "string", enum: ALLOWED_METHODS },
282
- price: { type: ["string", "number"] },
283
- description: { type: "string" },
284
- input_schema: { type: "object", additionalProperties: true },
285
- output_schema: { type: "object", additionalProperties: true },
286
- },
287
- },
288
- },
289
- },
290
- },
291
- },
292
- ];
293
-
294
- export const PUBLISHER_TOOL_NAMES = new Set(PUBLISHER_TOOLS.map((tool) => tool.name));
295
-
296
- export function isPublisherTool(name) {
297
- return PUBLISHER_TOOL_NAMES.has(name);
298
- }
299
-
300
- export function isProviderApiKey(token) {
301
- return typeof token === "string" && token.trim().toLowerCase().startsWith(PROVIDER_KEY_PREFIX);
302
- }
303
-
304
- export function slugify(value) {
305
- return trimString(value)
306
- .toLowerCase()
307
- .replace(/[^a-z0-9]+/g, "-")
308
- .replace(/^-+|-+$/g, "")
309
- .slice(0, 64);
310
- }
311
-
312
- export function normalizePath(value, fallback = "/") {
313
- let path = trimString(value);
314
- if (!path) return fallback;
315
- if (!path.startsWith("/")) path = `/${path}`;
316
- path = path.replace(/\/{2,}/g, "/");
317
- if (path.length > 1) path = path.replace(/\/+$/, "");
318
- if (!/^\/[A-Za-z0-9\-._~/{}%]*$/.test(path)) {
319
- throw new Error(`Invalid route path: ${JSON.stringify(value)}. Use URL-safe characters, e.g. /weather.`);
320
- }
321
- return path;
322
- }
323
-
324
- // The gateway forwards <origin_url> + <endpoint path>. When the agent hands
325
- // us a full upstream URL that already ends with the public path, strip it so
326
- // the path isn't doubled on forward.
327
- export function deriveOriginUrl(upstreamUrl, path) {
328
- const url = new URL(trimString(upstreamUrl));
329
- if (url.protocol !== "https:") {
330
- throw new Error("upstream_url must be HTTPS.");
331
- }
332
-
333
- const cleanPath = trimString(path);
334
- let pathname = url.pathname.replace(/\/+$/, "");
335
- if (cleanPath && cleanPath !== "/" && pathname.toLowerCase().endsWith(cleanPath.toLowerCase())) {
336
- pathname = pathname.slice(0, pathname.length - cleanPath.length);
337
- }
338
-
339
- return `${url.origin}${pathname}`.replace(/\/+$/, "") || url.origin;
340
- }
341
-
342
- export function defaultPathFromUpstream(upstreamUrl) {
343
- try {
344
- const segments = new URL(trimString(upstreamUrl)).pathname
345
- .split("/")
346
- .filter(Boolean);
347
- const last = segments[segments.length - 1];
348
- return last ? normalizePath(last) : "/";
349
- } catch {
350
- return "/";
351
- }
352
- }
353
-
354
- export function parsePriceUsdc(value) {
355
- const raw = typeof value === "number" ? value : Number.parseFloat(trimString(value));
356
- if (!Number.isFinite(raw) || raw <= 0) {
357
- throw new Error("price must be a positive USDC amount per call, e.g. \"0.01\".");
358
- }
359
- if (raw > MAX_PRICE_USDC) {
360
- throw new Error(`price exceeds the ${MAX_PRICE_USDC} USDC per-call maximum.`);
361
- }
362
- // USDC has 6 decimals; anything finer can't settle on-chain.
363
- return Math.round(raw * 1e6) / 1e6;
364
- }
365
-
366
- function validateSettlementAddress(address, network) {
367
- const value = trimString(address);
368
- if (network === "solana") {
369
- if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value)) {
370
- throw new Error("settlement_address must be a base58 Solana address for network 'solana'.");
371
- }
372
- return value;
373
- }
374
- if (!/^0x[0-9a-fA-F]{40}$/.test(value)) {
375
- throw new Error("settlement_address must be a 0x-prefixed EVM address.");
376
- }
377
- return value;
378
- }
379
-
380
- function resolveNetwork(value) {
381
- const network = trimString(value).toLowerCase() || "base";
382
- if (!ALLOWED_NETWORKS.includes(network)) {
383
- throw new Error(`Unsupported network '${network}'. Supported: ${ALLOWED_NETWORKS.join(", ")}.`);
384
- }
385
- return network;
386
- }
387
-
388
- function resolveMethod(value) {
389
- const method = trimString(value).toUpperCase() || "GET";
390
- if (!ALLOWED_METHODS.includes(method)) {
391
- throw new Error(`Unsupported method '${method}'. Supported: ${ALLOWED_METHODS.join(", ")}.`);
392
- }
393
- return method;
394
- }
395
-
396
- function resolveGatewayBaseUrl(env = process.env) {
397
- return (
398
- trimString(env.APIOSK_GATEWAY) ||
399
- trimString(env.APIOSK_GATEWAY_URL) ||
400
- DEFAULT_GATEWAY_BASE_URL
401
- ).replace(/\/+$/, "");
402
- }
403
-
404
- function resolveMcpPublicBaseUrl(env = process.env) {
405
- return (
406
- trimString(env.APIOSK_MCP_PUBLIC_BASE_URL) || DEFAULT_MCP_PUBLIC_BASE_URL
407
- ).replace(/\/+$/, "");
408
- }
409
-
410
- function resolveSupabaseConfig(env = process.env) {
411
- const url = (
412
- trimString(env.APIOSK_SUPABASE_URL) ||
413
- trimString(env.SUPABASE_URL) ||
414
- DEFAULT_SUPABASE_URL
415
- ).replace(/\/+$/, "");
416
- const serviceRoleKey =
417
- trimString(env.APIOSK_SUPABASE_SERVICE_ROLE_KEY) ||
418
- trimString(env.SUPABASE_SERVICE_ROLE_KEY);
419
-
420
- if (!serviceRoleKey) {
421
- throw new Error(
422
- "Publisher tools are not configured on this server: missing APIOSK_SUPABASE_SERVICE_ROLE_KEY (or SUPABASE_SERVICE_ROLE_KEY)."
423
- );
424
- }
425
-
426
- return { url, serviceRoleKey };
427
- }
428
-
429
- function resolveFetch(ctx = {}) {
430
- return ctx.fetchImpl || globalThis.fetch;
431
- }
432
-
433
- async function supabaseRest(ctx, path, { method = "GET", body, headers = {} } = {}) {
434
- const { url, serviceRoleKey } = resolveSupabaseConfig(ctx.env);
435
- const fetchImpl = resolveFetch(ctx);
436
-
437
- const response = await fetchImpl(`${url}/rest/v1/${path}`, {
438
- method,
439
- headers: {
440
- apikey: serviceRoleKey,
441
- authorization: `Bearer ${serviceRoleKey}`,
442
- "content-type": "application/json",
443
- accept: "application/json",
444
- ...(method === "GET" ? {} : { prefer: "return=representation" }),
445
- ...headers,
446
- },
447
- body: body === undefined ? undefined : JSON.stringify(body),
448
- });
449
-
450
- const text = await response.text();
451
- let payload = null;
452
- if (text) {
453
- try {
454
- payload = JSON.parse(text);
455
- } catch {
456
- payload = text;
457
- }
458
- }
459
-
460
- if (!response.ok) {
461
- const message =
462
- (payload && typeof payload === "object" && (payload.message || payload.hint || payload.details)) ||
463
- (typeof payload === "string" ? payload.slice(0, 300) : `HTTP ${response.status}`);
464
- throw new Error(`Apiosk database request failed (${response.status}): ${message}`);
465
- }
466
-
467
- return payload;
468
- }
469
-
470
- export async function verifyProviderKey(token, { env = process.env, fetchImpl } = {}) {
471
- const secret = trimString(token);
472
- if (!isProviderApiKey(secret)) {
473
- throw new Error("Not an Apiosk provider token (expected sk_live_… prefix).");
474
- }
475
-
476
- const cached = providerKeyCache.get(secret);
477
- if (cached && cached.expiresAt > Date.now()) {
478
- return cached.context;
479
- }
480
-
481
- const rows = await supabaseRest(
482
- { env, fetchImpl },
483
- "rpc/verify_provider_api_key",
484
- { method: "POST", body: { p_secret: secret } }
485
- );
486
-
487
- const row = Array.isArray(rows) ? rows[0] : rows;
488
- const ownerId = trimString(row?.owner_id);
489
- if (!ownerId) {
490
- // Header-safe ASCII only: this message can end up inside a
491
- // WWW-Authenticate challenge header, where non-ASCII throws.
492
- throw new Error("Invalid or revoked Apiosk provider token. Mint one in the provider portal (Settings, API keys).");
493
- }
494
-
495
- const context = {
496
- ownerId,
497
- keyId: trimString(row?.key_id) || undefined,
498
- label: trimString(row?.label) || undefined,
499
- };
500
- providerKeyCache.set(secret, { context, expiresAt: Date.now() + PROVIDER_KEY_CACHE_TTL_MS });
501
- return context;
502
- }
503
-
504
- function extractProviderToken(authInfo, env = process.env) {
505
- const fromExtra = trimString(authInfo?.extra?.apiosk_provider_key);
506
- if (isProviderApiKey(fromExtra)) return fromExtra;
507
-
508
- const fromToken = trimString(authInfo?.token);
509
- if (isProviderApiKey(fromToken)) return fromToken;
510
-
511
- const fromEnv =
512
- trimString(env.APIOSK_PROVIDER_TOKEN) ||
513
- trimString(env.APIOSK_PROVIDER_API_KEY) ||
514
- trimString(env.APIOSK_PROVIDER_KEY);
515
- if (isProviderApiKey(fromEnv)) return fromEnv;
516
-
517
- return null;
518
- }
519
-
520
- async function requireProvider(authInfo, ctx) {
521
- const token = extractProviderToken(authInfo, ctx.env);
522
- if (!token) {
523
- throw new Error(
524
- "Publisher tools need an Apiosk provider token. Connect with header `Authorization: Bearer sk_live_...` (mint the key in the provider portal under Settings, API keys), or set APIOSK_PROVIDER_TOKEN for local stdio use."
525
- );
526
- }
527
-
528
- const context = await verifyProviderKey(token, { env: ctx.env, fetchImpl: ctx.fetchImpl });
529
- return { ...context, token };
530
- }
531
-
532
- function mapListingStatus(status) {
533
- const value = trimString(status).toLowerCase();
534
- if (value === "active") return "active";
535
- if (value === "pending") return "pending_review";
536
- return "disabled";
537
- }
538
-
539
- function buildPaidUrl(env, slug, path) {
540
- const base = resolveGatewayBaseUrl(env);
541
- const routePath = path === "/" ? "" : path;
542
- return `${base}/${slug}${routePath}`;
543
- }
544
-
545
- function routeResponse(env, api, endpoint) {
546
- const status = mapListingStatus(api.status);
547
- const metadata = api.listing_metadata && typeof api.listing_metadata === "object" ? api.listing_metadata : {};
548
- return {
549
- route_id: endpoint.id,
550
- name: api.name,
551
- description: api.description || undefined,
552
- paid_url: buildPaidUrl(env, api.slug, endpoint.path),
553
- upstream_url: api.origin_url || api.endpoint_url || undefined,
554
- method: endpoint.method,
555
- path: endpoint.path,
556
- price: String(endpoint.price),
557
- currency: "USDC",
558
- network: trimString(metadata.x402_network) || "base",
559
- settlement_address: api.wallet_address || undefined,
560
- tags: Array.isArray(metadata.tags) && metadata.tags.length ? metadata.tags : undefined,
561
- status,
562
- };
563
- }
564
-
565
- function activationNote(api) {
566
- const status = mapListingStatus(api.status);
567
- if (status === "active") {
568
- return "Route is live: the paid URL returns 402 with an x402 payment offer until paid, then forwards to your upstream.";
569
- }
570
- if (status === "pending_review") {
571
- return "Route created and queued for Apiosk review (all new listings are approved by an operator before going live). Once approved it serves x402 payments, appears in gateway.apiosk.com/.well-known/x402, and is auto-indexed in the Coinbase x402 Bazaar. Use test_x402_route to verify once active.";
572
- }
573
- return "Route is disabled. Re-enable with update_x402_route {status: 'active'}.";
574
- }
575
-
576
- async function fetchRouteWithApi(ctx, routeId) {
577
- const id = trimString(routeId);
578
- if (!id) throw new Error("Missing required field: route_id");
579
-
580
- const rows = await supabaseRest(
581
- ctx,
582
- `api_endpoints?id=eq.${encodeURIComponent(id)}&select=*,apis(*)`
583
- );
584
- const row = Array.isArray(rows) ? rows[0] : null;
585
- if (!row || !row.apis) {
586
- throw new Error(`No route found for route_id ${id}. Use list_x402_routes to see your routes.`);
587
- }
588
-
589
- const { apis: api, ...endpoint } = row;
590
- return { api, endpoint };
591
- }
592
-
593
- function assertRouteOwnership(api, provider) {
594
- if (trimString(api.owner_id) !== provider.ownerId) {
595
- throw new Error("This route belongs to a different provider account.");
596
- }
597
- }
598
-
599
- async function findAvailableSlug(ctx, desiredSlug, ownerId) {
600
- const base = slugify(desiredSlug);
601
- if (!base) throw new Error("Could not derive a slug from the provided name.");
602
-
603
- for (let attempt = 0; attempt < 10; attempt += 1) {
604
- const candidate = attempt === 0 ? base : `${base}-${attempt + 1}`;
605
- const rows = await supabaseRest(
606
- ctx,
607
- `apis?slug=eq.${encodeURIComponent(candidate)}&select=id,owner_id,slug`
608
- );
609
- const existing = Array.isArray(rows) ? rows[0] : null;
610
- if (!existing) {
611
- return { slug: candidate, existing: null };
612
- }
613
- if (trimString(existing.owner_id) === ownerId) {
614
- return { slug: candidate, existing };
615
- }
616
- }
617
-
618
- throw new Error(`Slug '${base}' and its variants are taken. Pass an explicit unique 'slug'.`);
619
- }
620
-
621
- // Best-effort resolution of the multi-chain endpoint token (migration 047).
622
- // Base is the gateway default (NULL network_token_id), so failure to resolve
623
- // a non-base token degrades to Base settlement with a warning, not an error.
624
- async function resolveNetworkTokenId(ctx, network) {
625
- if (network === "base") return { networkTokenId: null, warning: null };
626
-
627
- try {
628
- const networks = await supabaseRest(
629
- ctx,
630
- `networks?name=eq.${encodeURIComponent(network)}&is_testnet=eq.false&select=id`
631
- );
632
- const networkId = Array.isArray(networks) ? trimString(networks[0]?.id) : "";
633
- if (!networkId) throw new Error("network not found");
634
-
635
- const tokens = await supabaseRest(
636
- ctx,
637
- `network_tokens?network_id=eq.${encodeURIComponent(networkId)}&select=id,tokens(symbol)`
638
- );
639
- const match = (Array.isArray(tokens) ? tokens : []).find(
640
- (row) => trimString(row?.tokens?.symbol).toUpperCase() === "USDC"
641
- );
642
- if (!match) throw new Error("USDC token not registered for network");
643
-
644
- return { networkTokenId: match.id, warning: null };
645
- } catch (error) {
646
- return {
647
- networkTokenId: null,
648
- warning: `Could not bind the route to ${network} USDC (${error.message}); it will settle on Base USDC (the gateway default). You can change the network later in the provider portal.`,
649
- };
650
- }
651
- }
652
-
653
- function buildListingMetadataPatch(existing, { tags, network, publisherExtras } = {}) {
654
- const base = existing && typeof existing === "object" ? { ...existing } : {};
655
- if (Array.isArray(tags) && tags.length) {
656
- base.tags = tags.map((tag) => trimString(tag)).filter(Boolean);
657
- }
658
- if (network) {
659
- base.x402_network = network;
660
- }
661
- base.publisher = "mcp-x402-publisher";
662
- if (publisherExtras && typeof publisherExtras === "object") {
663
- Object.assign(base, publisherExtras);
664
- }
665
- return base;
666
- }
667
-
668
- async function upsertListing(ctx, provider, input) {
669
- const network = resolveNetwork(input.network);
670
- const settlementAddress = validateSettlementAddress(input.settlement_address, network);
671
- const { slug, existing } = await findAvailableSlug(ctx, input.slug || input.name, provider.ownerId);
672
-
673
- const listingFields = {
674
- name: trimString(input.name),
675
- description: trimString(input.description) || trimString(input.name),
676
- origin_url: input.origin_url,
677
- endpoint_url: input.origin_url,
678
- wallet_address: settlementAddress,
679
- category: trimString(input.category) || "data",
680
- listing_type: "api",
681
- };
682
-
683
- if (existing) {
684
- const patched = await supabaseRest(
685
- ctx,
686
- `apis?id=eq.${encodeURIComponent(existing.id)}&select=*`,
687
- {
688
- method: "PATCH",
689
- body: {
690
- ...listingFields,
691
- listing_metadata: buildListingMetadataPatch(await fetchListingMetadata(ctx, existing.id), {
692
- tags: input.tags,
693
- network,
694
- }),
695
- },
696
- }
697
- );
698
- return { api: Array.isArray(patched) ? patched[0] : patched, network };
699
- }
700
-
701
- const inserted = await supabaseRest(ctx, "apis?select=*", {
702
- method: "POST",
703
- body: {
704
- ...listingFields,
705
- owner_id: provider.ownerId,
706
- slug,
707
- // The api-review gate (migration admin/0004) forces fresh publishes to
708
- // 'pending' regardless; writing it explicitly keeps intent obvious.
709
- status: "pending",
710
- listing_metadata: buildListingMetadataPatch(null, { tags: input.tags, network }),
711
- },
712
- });
713
-
714
- return { api: Array.isArray(inserted) ? inserted[0] : inserted, network };
715
- }
716
-
717
- async function fetchListingMetadata(ctx, apiId) {
718
- const rows = await supabaseRest(
719
- ctx,
720
- `apis?id=eq.${encodeURIComponent(apiId)}&select=listing_metadata`
721
- );
722
- return Array.isArray(rows) ? rows[0]?.listing_metadata : null;
723
- }
724
-
725
- async function upsertEndpoint(ctx, apiId, { method, path, price, description, inputSchema, outputSchema, networkTokenId }) {
726
- const existing = await supabaseRest(
727
- ctx,
728
- `api_endpoints?api_id=eq.${encodeURIComponent(apiId)}&method=eq.${encodeURIComponent(method)}&path=eq.${encodeURIComponent(path)}&select=id`
729
- );
730
- const existingId = Array.isArray(existing) ? trimString(existing[0]?.id) : "";
731
-
732
- const fields = {
733
- method,
734
- path,
735
- price,
736
- payment_required: price > 0,
737
- description: trimString(description) || undefined,
738
- request_body: inputSchema && Object.keys(inputSchema).length ? inputSchema : undefined,
739
- response_body: outputSchema && Object.keys(outputSchema).length ? outputSchema : undefined,
740
- };
741
- if (networkTokenId) {
742
- fields.network_token_id = networkTokenId;
743
- }
744
- for (const key of Object.keys(fields)) {
745
- if (fields[key] === undefined) delete fields[key];
746
- }
747
-
748
- if (existingId) {
749
- const patched = await supabaseRest(
750
- ctx,
751
- `api_endpoints?id=eq.${encodeURIComponent(existingId)}&select=*`,
752
- { method: "PATCH", body: fields }
753
- );
754
- return Array.isArray(patched) ? patched[0] : patched;
755
- }
756
-
757
- const inserted = await supabaseRest(ctx, "api_endpoints?select=*", {
758
- method: "POST",
759
- body: { api_id: apiId, ...fields },
760
- });
761
- return Array.isArray(inserted) ? inserted[0] : inserted;
762
- }
763
-
764
- async function handlePublishRoute(args, authInfo, ctx) {
765
- const provider = await requireProvider(authInfo, ctx);
766
-
767
- const name = trimString(args.name);
768
- if (!name) throw new Error("Missing required field: name");
769
- if (trimString(args.currency) && trimString(args.currency).toUpperCase() !== "USDC") {
770
- throw new Error("Only USDC pricing is supported.");
771
- }
772
-
773
- const method = resolveMethod(args.method);
774
- const path = args.path ? normalizePath(args.path) : defaultPathFromUpstream(args.upstream_url);
775
- const price = parsePriceUsdc(args.price);
776
- const originUrl = deriveOriginUrl(args.upstream_url, path);
777
- const network = resolveNetwork(args.network);
778
-
779
- const { api } = await upsertListing(ctx, provider, {
780
- name,
781
- description: args.description,
782
- origin_url: originUrl,
783
- settlement_address: args.settlement_address,
784
- network,
785
- tags: args.tags,
786
- slug: args.slug,
787
- });
788
-
789
- const { networkTokenId, warning } = await resolveNetworkTokenId(ctx, network);
790
- const endpoint = await upsertEndpoint(ctx, api.id, {
791
- method,
792
- path,
793
- price,
794
- description: args.description || name,
795
- inputSchema: args.input_schema,
796
- outputSchema: args.output_schema,
797
- networkTokenId,
798
- });
799
-
800
- const result = {
801
- ...routeResponse(ctx.env, api, endpoint),
802
- note: activationNote(api),
803
- };
804
- if (warning) result.warning = warning;
805
- return content(result);
806
- }
807
-
808
- async function handleListRoutes(args, authInfo, ctx) {
809
- const provider = await requireProvider(authInfo, ctx);
810
-
811
- const listings = await supabaseRest(
812
- ctx,
813
- `apis?owner_id=eq.${encodeURIComponent(provider.ownerId)}&select=*,api_endpoints(*)&order=created_at.desc`
814
- );
815
-
816
- const statusFilter = trimString(args.status);
817
- const routes = [];
818
- for (const api of Array.isArray(listings) ? listings : []) {
819
- for (const endpoint of Array.isArray(api.api_endpoints) ? api.api_endpoints : []) {
820
- const route = routeResponse(ctx.env, api, endpoint);
821
- if (statusFilter && route.status !== statusFilter) continue;
822
- routes.push(route);
823
- }
824
- }
825
-
826
- return content({ routes, count: routes.length });
827
- }
828
-
829
- async function handleUpdateRoute(args, authInfo, ctx) {
830
- const provider = await requireProvider(authInfo, ctx);
831
- const { api, endpoint } = await fetchRouteWithApi(ctx, args.route_id);
832
- assertRouteOwnership(api, provider);
833
-
834
- const endpointPatch = {};
835
- if (args.price !== undefined) {
836
- endpointPatch.price = parsePriceUsdc(args.price);
837
- endpointPatch.payment_required = endpointPatch.price > 0;
838
- }
839
- if (args.method !== undefined) endpointPatch.method = resolveMethod(args.method);
840
- if (args.path !== undefined) endpointPatch.path = normalizePath(args.path);
841
- if (args.input_schema !== undefined) endpointPatch.request_body = args.input_schema;
842
- if (args.output_schema !== undefined) endpointPatch.response_body = args.output_schema;
843
-
844
- const listingPatch = {};
845
- if (args.name !== undefined) listingPatch.name = trimString(args.name);
846
- if (args.description !== undefined) listingPatch.description = trimString(args.description);
847
- if (args.settlement_address !== undefined) {
848
- const metadata = api.listing_metadata && typeof api.listing_metadata === "object" ? api.listing_metadata : {};
849
- listingPatch.wallet_address = validateSettlementAddress(
850
- args.settlement_address,
851
- trimString(metadata.x402_network) || "base"
852
- );
853
- }
854
- if (args.upstream_url !== undefined) {
855
- const path = endpointPatch.path || endpoint.path;
856
- const origin = deriveOriginUrl(args.upstream_url, path);
857
- listingPatch.origin_url = origin;
858
- listingPatch.endpoint_url = origin;
859
- }
860
- if (args.tags !== undefined) {
861
- listingPatch.listing_metadata = buildListingMetadataPatch(api.listing_metadata, { tags: args.tags });
862
- }
863
- if (args.status !== undefined) {
864
- if (trimString(args.status) === "disabled") {
865
- listingPatch.status = "inactive";
866
- } else {
867
- // Service-role writes bypass the provider-only DB trigger, so every MCP
868
- // publish/resume request explicitly enters the admin review queue.
869
- listingPatch.status = "pending";
870
- }
871
- }
872
-
873
- let updatedEndpoint = endpoint;
874
- if (Object.keys(endpointPatch).length) {
875
- const rows = await supabaseRest(
876
- ctx,
877
- `api_endpoints?id=eq.${encodeURIComponent(endpoint.id)}&select=*`,
878
- { method: "PATCH", body: endpointPatch }
879
- );
880
- updatedEndpoint = (Array.isArray(rows) ? rows[0] : rows) || endpoint;
881
- }
882
-
883
- let updatedApi = api;
884
- if (Object.keys(listingPatch).length) {
885
- const rows = await supabaseRest(
886
- ctx,
887
- `apis?id=eq.${encodeURIComponent(api.id)}&select=*`,
888
- { method: "PATCH", body: listingPatch }
889
- );
890
- updatedApi = (Array.isArray(rows) ? rows[0] : rows) || api;
891
- }
892
-
893
- return content({
894
- ...routeResponse(ctx.env, updatedApi, updatedEndpoint),
895
- note: activationNote(updatedApi),
896
- });
897
- }
898
-
899
- async function handleUnpublishRoute(args, authInfo, ctx) {
900
- const provider = await requireProvider(authInfo, ctx);
901
- const { api, endpoint } = await fetchRouteWithApi(ctx, args.route_id);
902
- assertRouteOwnership(api, provider);
903
-
904
- const rows = await supabaseRest(
905
- ctx,
906
- `apis?id=eq.${encodeURIComponent(api.id)}&select=*,api_endpoints(id)`,
907
- { method: "PATCH", body: { status: "inactive" } }
908
- );
909
- const updated = (Array.isArray(rows) ? rows[0] : rows) || api;
910
- const siblingCount = Array.isArray(updated.api_endpoints)
911
- ? Math.max(0, updated.api_endpoints.length - 1)
912
- : 0;
913
-
914
- const result = {
915
- route_id: endpoint.id,
916
- status: "disabled",
917
- note: "Listing disabled: the paid URL no longer serves. Re-enable with update_x402_route {status: 'active'}.",
918
- };
919
- if (siblingCount > 0) {
920
- result.warning = `This route shares its gateway listing '${api.slug}' with ${siblingCount} other route(s); disabling took the whole listing offline.`;
921
- }
922
- return content(result);
923
- }
924
-
925
- async function handleTestRoute(args, authInfo, ctx) {
926
- const provider = await requireProvider(authInfo, ctx);
927
- const { api, endpoint } = await fetchRouteWithApi(ctx, args.route_id);
928
- assertRouteOwnership(api, provider);
929
-
930
- const paidUrl = buildPaidUrl(ctx.env, api.slug, endpoint.path);
931
- const payload = args.test_payload && typeof args.test_payload === "object" ? args.test_payload : null;
932
-
933
- const url = new URL(paidUrl);
934
- const requestInit = { method: endpoint.method, headers: { accept: "application/json" } };
935
- if (payload) {
936
- if (endpoint.method === "GET") {
937
- for (const [key, value] of Object.entries(payload)) {
938
- url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
939
- }
940
- } else {
941
- requestInit.headers["content-type"] = "application/json";
942
- requestInit.body = JSON.stringify(payload);
943
- }
944
- }
945
-
946
- const fetchImpl = resolveFetch(ctx);
947
- let response;
948
- try {
949
- response = await fetchImpl(url.href, requestInit);
950
- } catch (error) {
951
- return content({
952
- success: false,
953
- paid_url: paidUrl,
954
- error: `Gateway unreachable: ${error.message}`,
955
- });
956
- }
957
-
958
- const text = await response.text();
959
- let body = null;
960
- try {
961
- body = text ? JSON.parse(text) : null;
962
- } catch {
963
- body = text.slice(0, 500);
964
- }
965
-
966
- const accepts = body && typeof body === "object" && Array.isArray(body.accepts) ? body.accepts : null;
967
- const x402Enabled = response.status === 402 && Boolean(accepts?.length) && body?.x402Version != null;
968
- const listingStatus = mapListingStatus(api.status);
969
-
970
- let responsePreview = body;
971
- if (body && typeof body === "object") {
972
- const serialized = JSON.stringify(body);
973
- if (serialized.length > 4000) {
974
- responsePreview = `${serialized.slice(0, 4000)}…`;
975
- }
976
- }
977
-
978
- const result = {
979
- success: response.status === 402 ? x402Enabled : response.ok,
980
- http_status: response.status,
981
- paid_url: paidUrl,
982
- payment_required: response.status === 402,
983
- x402_enabled: x402Enabled,
984
- listing_status: listingStatus,
985
- response_preview: responsePreview,
986
- };
987
-
988
- if (accepts?.length) {
989
- const offer = accepts[0];
990
- result.payment_offer = {
991
- scheme: offer.scheme,
992
- network: friendlyNetworkName(offer.network),
993
- // x402 v2 uses `amount`; v1 used `maxAmountRequired`.
994
- max_amount_required: offer.amount ?? offer.maxAmountRequired,
995
- pay_to: offer.payTo,
996
- asset: offer.asset,
997
- };
998
- }
999
-
1000
- if (response.status === 404 && listingStatus !== "active") {
1001
- result.hint =
1002
- listingStatus === "pending_review"
1003
- ? "The route is still awaiting Apiosk review approval, so the gateway does not serve it yet. Re-run this test once it is approved."
1004
- : "The route's listing is disabled. Re-enable it with update_x402_route {status: 'active'}.";
1005
- }
1006
-
1007
- return content(result);
1008
- }
1009
-
1010
- export function buildOpenApiDocument(env, api, endpoints, overrides = {}) {
1011
- const metadata = api.listing_metadata && typeof api.listing_metadata === "object" ? api.listing_metadata : {};
1012
- const stored = metadata.openapi && typeof metadata.openapi === "object" ? metadata.openapi : {};
1013
- const paths = {};
1014
-
1015
- for (const endpoint of endpoints) {
1016
- const path = endpoint.path || "/";
1017
- paths[path] = paths[path] || {};
1018
- const operation = {
1019
- operationId: `${trimString(endpoint.method).toLowerCase()}${path.replace(/[^A-Za-z0-9]+/g, "_")}`,
1020
- summary: endpoint.description || api.name,
1021
- "x-price": { amount: String(endpoint.price), currency: "USDC", per: "request" },
1022
- "x-payment-protocol": "x402",
1023
- responses: {
1024
- 200: { description: "Successful, paid response." },
1025
- 402: {
1026
- description:
1027
- "Payment required. Body contains an x402 payment offer (x402Version, accepts[]). Pay via the x402 protocol and retry with the X-Payment header.",
1028
- },
1029
- },
1030
- };
1031
-
1032
- if (endpoint.request_body && typeof endpoint.request_body === "object" && Object.keys(endpoint.request_body).length) {
1033
- if (endpoint.method === "GET") {
1034
- operation["x-input-schema"] = endpoint.request_body;
1035
- } else {
1036
- operation.requestBody = {
1037
- content: { "application/json": { schema: endpoint.request_body } },
1038
- };
1039
- }
1040
- }
1041
- if (endpoint.response_body && typeof endpoint.response_body === "object" && Object.keys(endpoint.response_body).length) {
1042
- operation.responses[200].content = {
1043
- "application/json": { schema: endpoint.response_body },
1044
- };
1045
- }
1046
-
1047
- paths[path][trimString(endpoint.method).toLowerCase()] = operation;
1048
- }
1049
-
1050
- for (const extra of Array.isArray(overrides.endpoints) ? overrides.endpoints : []) {
1051
- if (extra && typeof extra === "object" && trimString(extra.path)) {
1052
- paths[trimString(extra.path)] = { ...(paths[trimString(extra.path)] || {}), ...extra.pathItem };
1053
- }
1054
- }
1055
-
1056
- return {
1057
- openapi: "3.1.0",
1058
- info: {
1059
- title: trimString(overrides.title) || trimString(stored.title) || api.name,
1060
- version: trimString(overrides.version) || trimString(stored.version) || "1.0.0",
1061
- description: api.description || undefined,
1062
- },
1063
- servers: [{ url: buildPaidUrl(env, api.slug, "/").replace(/\/$/, "") }],
1064
- paths,
1065
- "x-x402": {
1066
- network: trimString(metadata.x402_network) || "base",
1067
- currency: "USDC",
1068
- discovery: `${resolveGatewayBaseUrl(env)}/.well-known/x402`,
1069
- },
1070
- };
1071
- }
1072
-
1073
- async function handleGenerateOpenApi(args, authInfo, ctx) {
1074
- const provider = await requireProvider(authInfo, ctx);
1075
- const { api, endpoint } = await fetchRouteWithApi(ctx, args.route_id);
1076
- assertRouteOwnership(api, provider);
1077
-
1078
- const overrides = {
1079
- title: trimString(args.title) || undefined,
1080
- version: trimString(args.version) || undefined,
1081
- endpoints: args.endpoints,
1082
- };
1083
-
1084
- const metadata = buildListingMetadataPatch(api.listing_metadata, {
1085
- publisherExtras: {
1086
- openapi: {
1087
- title: overrides.title || api.name,
1088
- version: overrides.version || "1.0.0",
1089
- },
1090
- },
1091
- });
1092
- await supabaseRest(ctx, `apis?id=eq.${encodeURIComponent(api.id)}`, {
1093
- method: "PATCH",
1094
- body: { listing_metadata: metadata },
1095
- });
1096
-
1097
- const endpoints = await supabaseRest(
1098
- ctx,
1099
- `api_endpoints?api_id=eq.${encodeURIComponent(api.id)}&select=*`
1100
- );
1101
- const document = buildOpenApiDocument(
1102
- ctx.env,
1103
- { ...api, listing_metadata: metadata },
1104
- Array.isArray(endpoints) ? endpoints : [endpoint],
1105
- overrides
1106
- );
1107
-
1108
- return content({
1109
- route_id: endpoint.id,
1110
- openapi_url: `${resolveMcpPublicBaseUrl(ctx.env)}/openapi/${endpoint.id}.json`,
1111
- preview: { info: document.info, paths: Object.keys(document.paths) },
1112
- });
1113
- }
1114
-
1115
- async function handlePublishProject(args, authInfo, ctx) {
1116
- const provider = await requireProvider(authInfo, ctx);
1117
-
1118
- const projectName = trimString(args.project_name);
1119
- if (!projectName) throw new Error("Missing required field: project_name");
1120
- const routesInput = Array.isArray(args.routes) ? args.routes : [];
1121
- if (!routesInput.length) throw new Error("routes must contain at least one route.");
1122
-
1123
- const baseUrl = new URL(trimString(args.base_url));
1124
- if (baseUrl.protocol !== "https:") throw new Error("base_url must be HTTPS.");
1125
- const origin = `${baseUrl.origin}${baseUrl.pathname}`.replace(/\/+$/, "");
1126
- const network = resolveNetwork(args.network);
1127
-
1128
- const { api } = await upsertListing(ctx, provider, {
1129
- name: projectName,
1130
- description: args.description || projectName,
1131
- origin_url: origin,
1132
- settlement_address: args.settlement_address,
1133
- network,
1134
- tags: args.tags,
1135
- });
1136
-
1137
- const { networkTokenId, warning } = await resolveNetworkTokenId(ctx, network);
1138
- const routes = [];
1139
- for (const route of routesInput) {
1140
- const endpoint = await upsertEndpoint(ctx, api.id, {
1141
- method: resolveMethod(route.method),
1142
- path: normalizePath(route.path),
1143
- price: parsePriceUsdc(route.price),
1144
- description: route.description || route.name || projectName,
1145
- inputSchema: route.input_schema,
1146
- outputSchema: route.output_schema,
1147
- networkTokenId,
1148
- });
1149
- routes.push({
1150
- route_id: endpoint.id,
1151
- name: trimString(route.name) || projectName,
1152
- paid_url: buildPaidUrl(ctx.env, api.slug, endpoint.path),
1153
- method: endpoint.method,
1154
- path: endpoint.path,
1155
- price: String(endpoint.price),
1156
- });
1157
- }
1158
-
1159
- const result = {
1160
- project_id: api.id,
1161
- project_slug: api.slug,
1162
- status: mapListingStatus(api.status),
1163
- routes_created: routes.length,
1164
- routes,
1165
- note: activationNote(api),
1166
- };
1167
- if (warning) result.warning = warning;
1168
- return content(result);
1169
- }
1170
-
1171
- // Public (unauthenticated) helpers used by server.mjs HTTP routes. ---------
1172
-
1173
- export async function getOpenApiRouteDocument(routeId, { env = process.env, fetchImpl } = {}) {
1174
- const ctx = { env, fetchImpl };
1175
- const id = trimString(routeId).replace(/\.json$/i, "");
1176
- if (!id) return null;
1177
-
1178
- let route;
1179
- try {
1180
- route = await fetchRouteWithApi(ctx, id);
1181
- } catch {
1182
- return null;
1183
- }
1184
-
1185
- const endpoints = await supabaseRest(
1186
- ctx,
1187
- `api_endpoints?api_id=eq.${encodeURIComponent(route.api.id)}&select=*`
1188
- );
1189
- return buildOpenApiDocument(
1190
- env,
1191
- route.api,
1192
- Array.isArray(endpoints) && endpoints.length ? endpoints : [route.endpoint]
1193
- );
1194
- }
1195
-
1196
- // CAIP-2 chain ids (x402 v2) → the friendly network names the publisher
1197
- // tools speak. Unknown ids pass through untouched.
1198
- const CAIP2_NETWORK_NAMES = {
1199
- "eip155:8453": "base",
1200
- "eip155:137": "polygon",
1201
- "eip155:42161": "arbitrum",
1202
- "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": "solana",
1203
- };
1204
-
1205
- function friendlyNetworkName(network) {
1206
- const value = trimString(network);
1207
- if (!value) return "base";
1208
- if (CAIP2_NETWORK_NAMES[value]) return CAIP2_NETWORK_NAMES[value];
1209
- if (value.startsWith("solana:")) return "solana";
1210
- return value;
1211
- }
1212
-
1213
- export function reshapeDiscoveryItems(env, wellKnownDocument) {
1214
- const items = Array.isArray(wellKnownDocument?.items) ? wellKnownDocument.items : [];
1215
- return items.map((item) => {
1216
- const offer = Array.isArray(item.accepts) ? item.accepts[0] : null;
1217
- const metadata = item.metadata && typeof item.metadata === "object" ? item.metadata : {};
1218
- // x402 v2 calls the atomic amount `amount`; v1 called it
1219
- // `maxAmountRequired`. Both are USDC 6-decimal atomic units.
1220
- const atomic = Number.parseInt(
1221
- trimString(offer?.amount ?? offer?.maxAmountRequired),
1222
- 10
1223
- );
1224
- return {
1225
- name: metadata.name || metadata.api || undefined,
1226
- description: metadata.description || undefined,
1227
- url: item.resource,
1228
- method: metadata.method || "GET",
1229
- price: Number.isFinite(atomic) ? (atomic / 1e6).toFixed(6).replace(/0+$/, "").replace(/\.$/, "") : undefined,
1230
- currency: "USDC",
1231
- network: friendlyNetworkName(offer?.network),
1232
- pay_to: offer?.payTo || undefined,
1233
- x402_version: item.x402Version ?? wellKnownDocument?.x402Version ?? 1,
1234
- };
1235
- });
1236
- }
1237
-
1238
- export async function buildDiscoveryDocument({ env = process.env, fetchImpl } = {}) {
1239
- if (discoveryCache && discoveryCache.expiresAt > Date.now()) {
1240
- return discoveryCache.document;
1241
- }
1242
-
1243
- const gateway = resolveGatewayBaseUrl(env);
1244
- const doFetch = fetchImpl || globalThis.fetch;
1245
- const response = await doFetch(`${gateway}/.well-known/x402`, {
1246
- headers: { accept: "application/json" },
1247
- });
1248
- if (!response.ok) {
1249
- throw new Error(`Gateway discovery document unavailable (HTTP ${response.status}).`);
1250
- }
1251
- const wellKnown = await response.json();
1252
-
1253
- const routes = reshapeDiscoveryItems(env, wellKnown);
1254
- const document = {
1255
- name: "Apiosk paid API routes",
1256
- description:
1257
- "Machine-readable index of paid x402 routes published through Apiosk. Each route returns 402 Payment Required with an x402 offer until paid in USDC.",
1258
- generated_from: `${gateway}/.well-known/x402`,
1259
- mcp_endpoint: `${resolveMcpPublicBaseUrl(env)}/mcp`,
1260
- count: routes.length,
1261
- routes,
1262
- };
1263
-
1264
- discoveryCache = { document, expiresAt: Date.now() + DISCOVERY_CACHE_TTL_MS };
1265
- return document;
1266
- }
1267
-
1268
- export function clearPublisherCaches() {
1269
- providerKeyCache.clear();
1270
- discoveryCache = null;
1271
- }
1272
-
1273
- export async function handlePublisherTool(name, args = {}, authInfo = null, ctx = {}) {
1274
- const context = { env: ctx.env || process.env, fetchImpl: ctx.fetchImpl };
1275
-
1276
- try {
1277
- if (name === "publish_x402_route") return await handlePublishRoute(args, authInfo, context);
1278
- if (name === "update_x402_route") return await handleUpdateRoute(args, authInfo, context);
1279
- if (name === "list_x402_routes") return await handleListRoutes(args, authInfo, context);
1280
- if (name === "unpublish_x402_route") return await handleUnpublishRoute(args, authInfo, context);
1281
- if (name === "test_x402_route") return await handleTestRoute(args, authInfo, context);
1282
- if (name === "generate_openapi_spec") return await handleGenerateOpenApi(args, authInfo, context);
1283
- if (name === "publish_project") return await handlePublishProject(args, authInfo, context);
1284
- return errorContent(`Unknown publisher tool: ${name}`);
1285
- } catch (error) {
1286
- return errorContent({ error: error instanceof Error ? error.message : String(error) });
1287
- }
1288
- }