@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
@@ -1,203 +0,0 @@
1
- // Apiosk external x402 payment (apiosk_fetch_paid).
2
- //
3
- // The last step of the agentic flow for endpoints the Apiosk gateway does NOT
4
- // host: pay an arbitrary external x402 URL from the connected buyer's managed
5
- // wallet. The MCP holds no signing keys, so this delegates to the gateway's
6
- // gateway-as-payer proxy (POST /v1/x402/fetch): the gateway enforces the connect
7
- // token's spend caps, pays the provider from a platform payer wallet, debits the
8
- // buyer's managed wallet, and returns the provider response.
9
- //
10
- // For Apiosk CATALOG listings use apiosk_execute instead — that path is cheaper
11
- // and fully settled. This tool is only for external (federated / discovered)
12
- // endpoints. `confirmed_price_usdc` is required and is the in-chat price
13
- // checkpoint: the gateway refuses if the live price exceeds what the user
14
- // confirmed via apiosk_inspect_x402.
15
-
16
- import { randomUUID } from "node:crypto";
17
-
18
- const DEFAULT_GATEWAY_BASE_URL = "https://gateway.apiosk.com";
19
-
20
- function content(value) {
21
- const result = {
22
- content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }],
23
- };
24
- if (value && typeof value === "object" && !Array.isArray(value)) {
25
- result.structuredContent = value;
26
- }
27
- return result;
28
- }
29
-
30
- function trimString(value) {
31
- return String(value ?? "").trim();
32
- }
33
-
34
- /**
35
- * Execute a paid fetch of an external x402 endpoint via the gateway payer proxy.
36
- *
37
- * @param {object} args - { url, method?, query?, body?, headers?, confirmed_price_usdc, max_price_usdc?, idempotency_key? }
38
- * @param {object} ctx - { connectToken, gatewayBaseUrl, fetchImpl? }
39
- */
40
- export async function runFetchPaid(args = {}, ctx = {}) {
41
- const url = trimString(args.url);
42
- if (!url) {
43
- return content({ status: "error", error: "Missing required field: url" });
44
- }
45
- const confirmedPrice = args.confirmed_price_usdc;
46
- if (!Number.isFinite(confirmedPrice) || confirmedPrice < 0) {
47
- return content({
48
- status: "error",
49
- error:
50
- "Missing required field: confirmed_price_usdc. Call apiosk_inspect_x402 on the URL first, tell the user the price, then pass the confirmed amount here.",
51
- });
52
- }
53
-
54
- const connectToken = trimString(ctx.connectToken);
55
- if (!connectToken) {
56
- return content({
57
- status: "error",
58
- error:
59
- "No connected wallet. apiosk_fetch_paid needs an Apiosk connect token (authorize the Apiosk app / OAuth on the hosted server). For Apiosk catalog listings, use apiosk_execute instead.",
60
- });
61
- }
62
-
63
- const gatewayBaseUrl = trimString(ctx.gatewayBaseUrl) || DEFAULT_GATEWAY_BASE_URL;
64
- const fetchImpl = ctx.fetchImpl || fetch;
65
- const idempotencyKey = trimString(args.idempotency_key) || randomUUID();
66
- const method = trimString(args.method).toUpperCase() || "GET";
67
-
68
- const requestBody = {
69
- url,
70
- method,
71
- query: args.query && typeof args.query === "object" ? args.query : undefined,
72
- body: args.body !== undefined ? args.body : undefined,
73
- headers: args.headers && typeof args.headers === "object" ? args.headers : undefined,
74
- confirmed_price_usdc: confirmedPrice,
75
- max_price_usdc: Number.isFinite(args.max_price_usdc) ? args.max_price_usdc : undefined,
76
- };
77
-
78
- let response;
79
- try {
80
- response = await fetchImpl(`${gatewayBaseUrl.replace(/\/+$/, "")}/v1/x402/fetch`, {
81
- method: "POST",
82
- cache: "no-store",
83
- headers: {
84
- "content-type": "application/json",
85
- accept: "application/json",
86
- "X-Apiosk-Connect-Token": connectToken,
87
- "Idempotency-Key": idempotencyKey,
88
- },
89
- body: JSON.stringify(requestBody),
90
- });
91
- } catch (error) {
92
- return content({
93
- status: "error",
94
- error: `Could not reach the Apiosk gateway payer: ${trimString(error?.message || error)}`,
95
- idempotency_key: idempotencyKey,
96
- });
97
- }
98
-
99
- const text = await response.text().catch(() => "");
100
- let payload = null;
101
- try {
102
- payload = text ? JSON.parse(text) : null;
103
- } catch {
104
- payload = text || null;
105
- }
106
-
107
- // The gateway emits refusal codes under `error` (json_error_response) or
108
- // `code`; normalize so callers below can read either.
109
- const gatewayCode = payload?.code || payload?.error || null;
110
-
111
- // Feature not enabled yet: degrade gracefully so the model falls back to a
112
- // catalog listing instead of surfacing a raw 403.
113
- if (response.status === 403 && gatewayCode === "feature_disabled") {
114
- return content({
115
- status: "unavailable",
116
- code: "feature_disabled",
117
- message:
118
- "External direct-pay (apiosk_fetch_paid) is not enabled on this gateway yet. Use an Apiosk catalog listing via apiosk_execute instead.",
119
- idempotency_key: idempotencyKey,
120
- });
121
- }
122
-
123
- // Any non-2xx with a structured code is a business outcome (price too high,
124
- // host not allowed, wallet over cap) — return it as data, not an MCP error, so
125
- // the model can adapt (lower the amount, pick another endpoint, tell the user).
126
- if (!response.ok) {
127
- return content({
128
- status: payload?.status || (response.status === 402 ? "payment_required" : "refused"),
129
- code: gatewayCode || `http_${response.status}`,
130
- message: payload?.message || payload?.error || `Gateway returned HTTP ${response.status}.`,
131
- http_status: response.status,
132
- idempotency_key: idempotencyKey,
133
- });
134
- }
135
-
136
- // Success: surface the provider data + a receipt the model can quote back.
137
- return content({
138
- status: "success",
139
- data: payload?.data ?? payload,
140
- receipt: payload?.receipt || null,
141
- idempotency_key: idempotencyKey,
142
- });
143
- }
144
-
145
- export const FETCH_PAID_TOOL = {
146
- name: "apiosk_fetch_paid",
147
- description:
148
- "Pay an EXTERNAL x402 endpoint (one Apiosk does not host) from the connected wallet and return its data. Use this only for external results from apiosk_discover (executable_via='apiosk_fetch_paid'); for Apiosk catalog listings use apiosk_execute. REQUIRED: call apiosk_inspect_x402 on the url first, tell the user the exact price, and pass that amount as confirmed_price_usdc — the gateway refuses if the live price is higher. The gateway enforces the wallet's per-tx/daily spend limits. Base + USDC only.",
149
- annotations: {
150
- readOnlyHint: false,
151
- destructiveHint: true,
152
- idempotentHint: false,
153
- openWorldHint: true,
154
- },
155
- _meta: {
156
- "openai/outputTemplate": "ui://apiosk/result-canvas.html",
157
- "openai/toolInvocation/invoking": "Paying the provider and fetching data…",
158
- "openai/toolInvocation/invoked": "Paid data received",
159
- ui: { resourceUri: "ui://apiosk/result-canvas.html" },
160
- },
161
- inputSchema: {
162
- type: "object",
163
- required: ["url", "confirmed_price_usdc"],
164
- properties: {
165
- url: {
166
- type: "string",
167
- description: "The external x402 resource URL to pay and fetch (from a discovery result's `url`).",
168
- },
169
- confirmed_price_usdc: {
170
- type: "number",
171
- description: "The price you read via apiosk_inspect_x402 and confirmed with the user. The gateway refuses if the live price exceeds this.",
172
- },
173
- method: {
174
- type: "string",
175
- enum: ["GET", "POST", "PUT", "PATCH", "DELETE"],
176
- description: "HTTP method for the provider request. Defaults to GET.",
177
- },
178
- query: {
179
- type: "object",
180
- additionalProperties: true,
181
- description: "Optional query parameters to send to the provider.",
182
- },
183
- body: {
184
- type: "object",
185
- additionalProperties: true,
186
- description: "Optional JSON request body for POST/PUT/PATCH.",
187
- },
188
- headers: {
189
- type: "object",
190
- additionalProperties: true,
191
- description: "Optional extra request headers (allowlisted server-side; secrets are never accepted here).",
192
- },
193
- max_price_usdc: {
194
- type: "number",
195
- description: "Optional additional per-call ceiling. The gateway also enforces the wallet's per-tx and daily limits.",
196
- },
197
- idempotency_key: {
198
- type: "string",
199
- description: "Optional. Reuse the same key to safely retry without paying twice; a fresh one is generated if omitted.",
200
- },
201
- },
202
- },
203
- };
@@ -1,255 +0,0 @@
1
- import { createHmac } from "node:crypto";
2
-
3
- import QRCode from "qrcode";
4
-
5
- const TRANSAK_API_URL = "https://api-gateway.transak.com";
6
- const ONRAMPER_WIDGET_URL = "https://buy.onramper.com";
7
- const BASE_EXPLORER_URL = "https://basescan.org";
8
-
9
- function trimString(value) {
10
- return String(value || "").trim();
11
- }
12
-
13
- function boolFromEnv(value, defaultValue = true) {
14
- const normalized = trimString(value).toLowerCase();
15
- if (!normalized) return defaultValue;
16
- if (["0", "false", "no", "off"].includes(normalized)) return false;
17
- if (["1", "true", "yes", "on"].includes(normalized)) return true;
18
- return defaultValue;
19
- }
20
-
21
- function qrImageUrl(value) {
22
- return `https://api.qrserver.com/v1/create-qr-code/?size=280x280&data=${encodeURIComponent(value)}`;
23
- }
24
-
25
- function getOnramperConfig(env = process.env) {
26
- return {
27
- apiKey: trimString(env.ONRAMPER_API_KEY),
28
- widgetSecret: trimString(env.ONRAMPER_WIDGET_SECRET),
29
- };
30
- }
31
-
32
- function getTransakConfig(env = process.env) {
33
- return {
34
- apiKey: trimString(env.TRANSAK_API_KEY),
35
- secret: trimString(env.TRANSAK_SECRET),
36
- referrerDomain: trimString(env.APIOSK_TRANSAK_REFERRER_DOMAIN || "dashboard.apiosk.com"),
37
- };
38
- }
39
-
40
- function buildOnramperWidgetUrl(address, env = process.env) {
41
- const { apiKey, widgetSecret } = getOnramperConfig(env);
42
- const normalizedAddress = trimString(address).toLowerCase();
43
- const searchParams = new URLSearchParams({
44
- apiKey,
45
- mode: "buy",
46
- defaultCrypto: "USDC",
47
- defaultNetwork: "base",
48
- onlyCryptoNetworks: "base",
49
- networkWallets: `base:${normalizedAddress}`,
50
- });
51
-
52
- const signContent = `networkWallets=base:${normalizedAddress}`;
53
- const signature = createHmac("sha256", widgetSecret)
54
- .update(signContent)
55
- .digest("hex");
56
-
57
- searchParams.set("signature", signature);
58
-
59
- return `${ONRAMPER_WIDGET_URL}/?${searchParams.toString()}`;
60
- }
61
-
62
- async function getTransakAccessToken(env = process.env, fetchImpl = globalThis.fetch) {
63
- const { apiKey, secret } = getTransakConfig(env);
64
- const response = await fetchImpl(`${TRANSAK_API_URL}/api/v2/auth/token`, {
65
- method: "POST",
66
- headers: { "Content-Type": "application/json" },
67
- body: JSON.stringify({
68
- apiKey,
69
- secretKey: secret,
70
- }),
71
- });
72
-
73
- if (!response.ok) {
74
- throw new Error("Failed to get Transak access token.");
75
- }
76
-
77
- const data = await response.json();
78
- return String(data.accessToken || "");
79
- }
80
-
81
- async function createTransakFundingSession(address, env = process.env, fetchImpl = globalThis.fetch) {
82
- const { apiKey, referrerDomain } = getTransakConfig(env);
83
- const accessToken = await getTransakAccessToken(env, fetchImpl);
84
-
85
- const sessionResponse = await fetchImpl(`${TRANSAK_API_URL}/api/v2/auth/session`, {
86
- method: "POST",
87
- headers: {
88
- "Content-Type": "application/json",
89
- "access-token": accessToken,
90
- },
91
- body: JSON.stringify({
92
- widgetParams: {
93
- apiKey,
94
- referrerDomain,
95
- walletAddress: address,
96
- cryptoCurrencyCode: "USDC",
97
- network: "base",
98
- defaultCryptoCurrency: "USDC",
99
- defaultNetwork: "base",
100
- disableWalletAddressForm: true,
101
- themeColor: "0052FF",
102
- hideMenu: true,
103
- },
104
- }),
105
- });
106
-
107
- if (!sessionResponse.ok) {
108
- throw new Error("Failed to create Transak funding session.");
109
- }
110
-
111
- const sessionData = await sessionResponse.json();
112
- return {
113
- widget_url: sessionData.widgetUrl || null,
114
- session_id: sessionData.sessionId || null,
115
- };
116
- }
117
-
118
- async function buildQrBundle(address, options = {}) {
119
- const env = options.env || process.env;
120
- const includeDataUrl =
121
- options.includeQrDataUrl === true || boolFromEnv(env.APIOSK_INCLUDE_QR_DATA_URL, false);
122
- const qrEnabled = boolFromEnv(env.APIOSK_ENABLE_QR, true);
123
- const transferUri = `ethereum:${address}@8453`;
124
- const payload = address;
125
- const bundle = {
126
- qr_payload: payload,
127
- transfer_uri: transferUri,
128
- qr_image_url: qrImageUrl(payload),
129
- };
130
-
131
- if (!qrEnabled) {
132
- return bundle;
133
- }
134
-
135
- try {
136
- bundle.qr_code_terminal = await QRCode.toString(payload, {
137
- type: "terminal",
138
- small: true,
139
- });
140
- bundle.qr_code_terminal_format = "ansi";
141
- } catch {
142
- // Leave terminal QR unset if rendering fails.
143
- }
144
-
145
- if (includeDataUrl) {
146
- try {
147
- bundle.qr_code_data_url = await QRCode.toDataURL(payload, {
148
- margin: 1,
149
- width: 280,
150
- });
151
- } catch {
152
- // Leave data URL unset if rendering fails.
153
- }
154
- }
155
-
156
- return bundle;
157
- }
158
-
159
- function buildFundingProviders(address, env = process.env) {
160
- const { apiKey: transakApiKey, secret: transakSecret } = getTransakConfig(env);
161
- const { apiKey: onramperApiKey, widgetSecret: onramperWidgetSecret } = getOnramperConfig(env);
162
-
163
- return [
164
- {
165
- id: "manual",
166
- label: "Receive on Base",
167
- description: "Send ETH or USDC on Base directly to this wallet address.",
168
- available: true,
169
- kind: "direct",
170
- address,
171
- explorer_url: `${BASE_EXPLORER_URL}/address/${address}`,
172
- },
173
- {
174
- id: "transak",
175
- label: "Transak",
176
- description: "Direct card or bank checkout for Base USDC.",
177
- available: Boolean(transakApiKey && transakSecret),
178
- kind: "direct",
179
- action_hint: "Call apiosk_configure with section='funding' and funding_provider='transak' to request a checkout URL.",
180
- },
181
- {
182
- id: "onramper",
183
- label: "Onramper",
184
- description: "Multi-provider onramp with outsourced KYC.",
185
- available: Boolean(onramperApiKey && onramperWidgetSecret),
186
- kind: "aggregator",
187
- action_hint: "Call apiosk_configure with section='funding' and funding_provider='onramper' to request a checkout URL.",
188
- },
189
- ];
190
- }
191
-
192
- export async function buildFundingOptions({
193
- wallet,
194
- env = process.env,
195
- fetchImpl = globalThis.fetch,
196
- fundingProvider = null,
197
- includeQrDataUrl = false,
198
- } = {}) {
199
- if (!wallet?.address) {
200
- return null;
201
- }
202
-
203
- const address = trimString(wallet.address).toLowerCase();
204
- const receiveOnBase = {
205
- title: "Receive on Base",
206
- description: "This address accepts both ETH and USDC on Base.",
207
- address,
208
- network: "Base",
209
- accepted_assets: ["ETH", "USDC"],
210
- explorer_url: `${BASE_EXPLORER_URL}/address/${address}`,
211
- ...await buildQrBundle(address, { env, includeQrDataUrl }),
212
- };
213
-
214
- const providers = buildFundingProviders(address, env);
215
- let selectedProvider = null;
216
- const requestedProvider = trimString(fundingProvider).toLowerCase();
217
-
218
- if (requestedProvider && requestedProvider !== "manual") {
219
- const provider = providers.find((entry) => entry.id === requestedProvider);
220
- if (provider) {
221
- if (!provider.available) {
222
- selectedProvider = {
223
- ...provider,
224
- error: `${provider.label} is not configured in this environment.`,
225
- };
226
- } else if (provider.id === "onramper") {
227
- selectedProvider = {
228
- ...provider,
229
- widget_url: buildOnramperWidgetUrl(address, env),
230
- opens_in: "browser",
231
- };
232
- } else if (provider.id === "transak") {
233
- const session = await createTransakFundingSession(address, env, fetchImpl);
234
- selectedProvider = {
235
- ...provider,
236
- ...session,
237
- opens_in: "browser",
238
- };
239
- }
240
- }
241
- } else if (requestedProvider === "manual") {
242
- selectedProvider = providers.find((entry) => entry.id === "manual") || null;
243
- }
244
-
245
- return {
246
- receive_on_base: receiveOnBase,
247
- providers,
248
- selected_provider: selectedProvider,
249
- notes: [
250
- "Base mainnet only.",
251
- "Manual funding works even when embedded onramp providers are unavailable.",
252
- "Use the wallet address or QR code from this section to top up the wallet.",
253
- ],
254
- };
255
- }
@@ -1,107 +0,0 @@
1
- import { randomBytes } from "node:crypto";
2
-
3
- import { privateKeyToAccount } from "viem/accounts";
4
-
5
- function parseBody(text) {
6
- if (!text) return null;
7
-
8
- try {
9
- return JSON.parse(text);
10
- } catch {
11
- return text;
12
- }
13
- }
14
-
15
- function pickMessage(payload, fallback) {
16
- if (payload && typeof payload === "object") {
17
- if (typeof payload.message === "string" && payload.message.trim()) {
18
- return payload.message.trim();
19
- }
20
- if (typeof payload.error === "string" && payload.error.trim()) {
21
- return payload.error.trim();
22
- }
23
- }
24
-
25
- if (typeof payload === "string" && payload.trim()) {
26
- return payload.trim();
27
- }
28
-
29
- return fallback;
30
- }
31
-
32
- export function buildWalletAuthMessage(action, resource, walletAddress, timestamp, nonce) {
33
- return [
34
- "Apiosk auth",
35
- `action:${action}`,
36
- `wallet:${String(walletAddress || "").toLowerCase()}`,
37
- `resource:${resource}`,
38
- `timestamp:${timestamp}`,
39
- `nonce:${nonce}`,
40
- ].join("\n");
41
- }
42
-
43
- async function signedRequest({
44
- baseUrl,
45
- path,
46
- method = "GET",
47
- body,
48
- action,
49
- resource,
50
- wallet,
51
- fetchImpl = globalThis.fetch,
52
- }) {
53
- if (!fetchImpl) {
54
- throw new Error("A fetch implementation is required.");
55
- }
56
-
57
- const account = privateKeyToAccount(wallet.private_key);
58
- const timestamp = String(Math.floor(Date.now() / 1000));
59
- const nonce = randomBytes(16).toString("hex");
60
- const message = buildWalletAuthMessage(action, resource, wallet.address, timestamp, nonce);
61
- const signature = await account.signMessage({ message });
62
- const headers = new Headers({
63
- accept: "application/json",
64
- "x-wallet-address": wallet.address,
65
- "x-wallet-signature": signature,
66
- "x-wallet-timestamp": timestamp,
67
- "x-wallet-nonce": nonce,
68
- });
69
-
70
- const init = {
71
- method,
72
- headers,
73
- };
74
-
75
- if (body !== undefined) {
76
- headers.set("content-type", "application/json");
77
- init.body = JSON.stringify(body);
78
- }
79
-
80
- const response = await fetchImpl(`${String(baseUrl).replace(/\/+$/, "")}${path}`, init);
81
- const text = await response.text();
82
- const payload = parseBody(text);
83
-
84
- return { payload, response };
85
- }
86
-
87
- export async function requestGatewayManagement(options) {
88
- for (let attempt = 0; attempt < 2; attempt += 1) {
89
- const { response, payload } = await signedRequest(options);
90
- if (response.ok) {
91
- return payload;
92
- }
93
-
94
- if ((response.status === 401 || response.status === 403) && attempt === 0) {
95
- continue;
96
- }
97
-
98
- throw new Error(
99
- pickMessage(
100
- payload,
101
- `Gateway management request failed with HTTP ${response.status}`
102
- )
103
- );
104
- }
105
-
106
- throw new Error("Gateway management request failed.");
107
- }