@apiosk/mcp 1.3.0 → 1.7.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 +135 -8
- package/docs/sepa-rail.md +13 -13
- package/dxt.json +7 -4
- package/logo-optimized-light.png +0 -0
- package/package.json +13 -8
- package/server.json +78 -4
- package/server.mjs +213 -11
- package/src/assets/wallet-accounts.mjs +20513 -0
- package/src/assets/walletconnect-provider.mjs +6319 -0
- package/src/create-server.mjs +47 -2
- package/src/discovery.mjs +929 -0
- package/src/external-fetch.mjs +203 -0
- package/src/hosted-payment.mjs +552 -0
- package/src/hosted-wallets.mjs +530 -0
- package/src/oauth.mjs +2011 -225
- package/src/observability.mjs +194 -0
- package/src/payment-guidance.mjs +15 -24
- package/src/publisher.mjs +1288 -0
- package/src/result-canvas.mjs +16 -0
- package/src/runtime.mjs +621 -30
- package/src/settlement-disclosure.mjs +26 -0
- package/src/source-registry.mjs +215 -0
- package/src/x402-inspect.mjs +361 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// Hosted managed-wallet tools over the signed-in user's own Supabase session.
|
|
2
|
+
//
|
|
3
|
+
// The DASHBOARD_WALLET_TOOLS were built against the legacy Next.js dashboard's
|
|
4
|
+
// /api/agent-wallets backend. That deployment is gone — dashboard.apiosk.com
|
|
5
|
+
// now serves the provider-portal SPA, whose catch-all rewrite answers every
|
|
6
|
+
// /api/* request with index.html. Proxying there returns HTML "200s" that MCP
|
|
7
|
+
// clients dutifully show the user as garbage.
|
|
8
|
+
//
|
|
9
|
+
// So on the hosted server every wallet tool talks straight to Supabase REST
|
|
10
|
+
// (PostgREST) with the caller's request-scoped session JWT as the bearer, the
|
|
11
|
+
// exact pattern the buyer portal uses client-side (buyer/src/lib/data/
|
|
12
|
+
// wallets.ts, connections.ts, wallet-calls.ts) and hosted-payment.mjs already
|
|
13
|
+
// uses for the sign-in connect-token mint. RLS applies as the signed-in user;
|
|
14
|
+
// the service role is never the acting identity.
|
|
15
|
+
//
|
|
16
|
+
// Custodial wallet CREATION stays unavailable here: deriving + encrypting the
|
|
17
|
+
// private key requires the legacy dashboard's AGENT_WALLET_SECRET_PEPPER and
|
|
18
|
+
// the per-user key-seed RPC, plus the on-chain settlement approval. Until that
|
|
19
|
+
// service is restored those tools explain the situation instead of failing
|
|
20
|
+
// with opaque HTML.
|
|
21
|
+
|
|
22
|
+
import { generateConnectToken, resolveSupabaseConfig, supabaseRest } from "./hosted-payment.mjs";
|
|
23
|
+
|
|
24
|
+
const DEFAULT_TOKEN_TTL_DAYS = 90;
|
|
25
|
+
const WALLET_SELECT =
|
|
26
|
+
"id,label,wallet_address,status,daily_limit_usdc,per_tx_limit_usdc,last_used_at,created_at,encrypted_private_key,icon,color";
|
|
27
|
+
const TOKEN_SELECT = "id,token_name,token_prefix,wallet_id,expires_at,created_at,revoked_at";
|
|
28
|
+
|
|
29
|
+
function trimString(value) {
|
|
30
|
+
return typeof value === "string" ? value.trim() : "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function asNumber(value) {
|
|
34
|
+
const parsed = Number(value);
|
|
35
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isoAfterDays(days) {
|
|
39
|
+
const date = new Date();
|
|
40
|
+
date.setDate(date.getDate() + days);
|
|
41
|
+
return date.toISOString();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function statusError(message, status = 500) {
|
|
45
|
+
const error = new Error(message);
|
|
46
|
+
error.status = status;
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function requireConfig(env) {
|
|
51
|
+
const config = resolveSupabaseConfig(env);
|
|
52
|
+
if (!config.configured) {
|
|
53
|
+
throw statusError("Managed wallet tools are not configured on this MCP server.", 503);
|
|
54
|
+
}
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireSession(sessionToken) {
|
|
59
|
+
const token = trimString(sessionToken);
|
|
60
|
+
if (!token) {
|
|
61
|
+
throw statusError(
|
|
62
|
+
"This tool needs your Apiosk session. Re-authorize the Apiosk app and retry.",
|
|
63
|
+
401
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return token;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Never leak encrypted_private_key; surface only whether the wallet is
|
|
70
|
+
// custodial (payable by the gateway) or watch-only.
|
|
71
|
+
function toWalletSummary(row) {
|
|
72
|
+
return {
|
|
73
|
+
id: row.id,
|
|
74
|
+
label: trimString(row.label) || "Managed wallet",
|
|
75
|
+
address: trimString(row.wallet_address).toLowerCase(),
|
|
76
|
+
status: row.status,
|
|
77
|
+
daily_limit_usdc: asNumber(row.daily_limit_usdc),
|
|
78
|
+
per_tx_limit_usdc: asNumber(row.per_tx_limit_usdc),
|
|
79
|
+
custodial: Boolean(trimString(row.encrypted_private_key)),
|
|
80
|
+
last_used_at: row.last_used_at || null,
|
|
81
|
+
created_at: row.created_at || null,
|
|
82
|
+
icon: row.icon ?? null,
|
|
83
|
+
color: row.color ?? null,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fundingInstructions(wallet) {
|
|
88
|
+
if (!wallet) return null;
|
|
89
|
+
return {
|
|
90
|
+
address: wallet.address,
|
|
91
|
+
network: "Base mainnet (chain 8453)",
|
|
92
|
+
asset: "USDC",
|
|
93
|
+
note: "Send USDC on Base mainnet to this address to fund paid API calls. Only Base mainnet USDC is accepted.",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function fetchWalletRows(config, sessionToken) {
|
|
98
|
+
const rows = await supabaseRest(
|
|
99
|
+
config,
|
|
100
|
+
`agent_wallets?select=${WALLET_SELECT}&order=created_at.desc&limit=8`,
|
|
101
|
+
{ sessionToken }
|
|
102
|
+
);
|
|
103
|
+
return Array.isArray(rows) ? rows.map(toWalletSummary) : [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function fetchWalletById(config, sessionToken, walletId) {
|
|
107
|
+
const rows = await supabaseRest(
|
|
108
|
+
config,
|
|
109
|
+
`agent_wallets?select=${WALLET_SELECT}&id=eq.${encodeURIComponent(walletId)}&limit=1`,
|
|
110
|
+
{ sessionToken }
|
|
111
|
+
);
|
|
112
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
113
|
+
if (!row) {
|
|
114
|
+
throw statusError("Wallet not found on your Apiosk account.", 404);
|
|
115
|
+
}
|
|
116
|
+
return toWalletSummary(row);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function resolveUserId(config, sessionToken, providedUserId) {
|
|
120
|
+
const uid = trimString(providedUserId);
|
|
121
|
+
if (uid) return uid;
|
|
122
|
+
|
|
123
|
+
const response = await fetch(`${config.supabaseUrl}/auth/v1/user`, {
|
|
124
|
+
headers: {
|
|
125
|
+
apikey: config.apiKey,
|
|
126
|
+
authorization: `Bearer ${sessionToken}`,
|
|
127
|
+
accept: "application/json",
|
|
128
|
+
},
|
|
129
|
+
cache: "no-store",
|
|
130
|
+
});
|
|
131
|
+
const body = await response.json().catch(() => ({}));
|
|
132
|
+
const id = trimString(body?.id);
|
|
133
|
+
if (!response.ok || !id) {
|
|
134
|
+
throw statusError("Could not resolve your Apiosk user from the session.", 401);
|
|
135
|
+
}
|
|
136
|
+
return id;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function hostedListWallets({ env = process.env, sessionToken } = {}) {
|
|
140
|
+
const config = requireConfig(env);
|
|
141
|
+
const token = requireSession(sessionToken);
|
|
142
|
+
const wallets = await fetchWalletRows(config, token);
|
|
143
|
+
const payable = wallets.find((wallet) => wallet.custodial && wallet.status === "active") || null;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
wallets,
|
|
147
|
+
payable_wallet: payable,
|
|
148
|
+
funding: fundingInstructions(payable),
|
|
149
|
+
note: payable
|
|
150
|
+
? "Paid API calls settle automatically from your payable managed wallet (per-call USDC on Base)."
|
|
151
|
+
: "No payable managed wallet found on this account. A custodial wallet with spend limits is required before paid calls can settle automatically.",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function hostedUpdateWallet({
|
|
156
|
+
env = process.env,
|
|
157
|
+
sessionToken,
|
|
158
|
+
walletId,
|
|
159
|
+
label,
|
|
160
|
+
status,
|
|
161
|
+
dailyLimitUsdc,
|
|
162
|
+
perTxLimitUsdc,
|
|
163
|
+
color,
|
|
164
|
+
icon,
|
|
165
|
+
} = {}) {
|
|
166
|
+
const config = requireConfig(env);
|
|
167
|
+
const token = requireSession(sessionToken);
|
|
168
|
+
const id = trimString(walletId);
|
|
169
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
170
|
+
|
|
171
|
+
const patch = {};
|
|
172
|
+
if (label !== undefined) {
|
|
173
|
+
const trimmed = trimString(label).slice(0, 64);
|
|
174
|
+
if (!trimmed) throw statusError("Wallet label must be 1 to 64 characters.", 400);
|
|
175
|
+
patch.label = trimmed;
|
|
176
|
+
}
|
|
177
|
+
if (status !== undefined) {
|
|
178
|
+
if (!["active", "paused", "revoked"].includes(status)) {
|
|
179
|
+
throw statusError("status must be active, paused, or revoked.", 400);
|
|
180
|
+
}
|
|
181
|
+
patch.status = status;
|
|
182
|
+
}
|
|
183
|
+
if (dailyLimitUsdc !== undefined || perTxLimitUsdc !== undefined) {
|
|
184
|
+
const daily = Number(dailyLimitUsdc);
|
|
185
|
+
const perTx = Number(perTxLimitUsdc);
|
|
186
|
+
if (!Number.isFinite(daily) || daily <= 0 || !Number.isFinite(perTx) || perTx <= 0) {
|
|
187
|
+
throw statusError("Provide both daily_limit_usdc and per_tx_limit_usdc as positive numbers.", 400);
|
|
188
|
+
}
|
|
189
|
+
if (perTx > daily) {
|
|
190
|
+
throw statusError("Per-request cap cannot exceed the daily budget.", 400);
|
|
191
|
+
}
|
|
192
|
+
patch.daily_limit_usdc = daily;
|
|
193
|
+
patch.per_tx_limit_usdc = perTx;
|
|
194
|
+
}
|
|
195
|
+
if (color !== undefined) patch.color = color || null;
|
|
196
|
+
if (icon !== undefined) patch.icon = icon || null;
|
|
197
|
+
if (Object.keys(patch).length === 0) throw statusError("Nothing to update.", 400);
|
|
198
|
+
|
|
199
|
+
const rows = await supabaseRest(
|
|
200
|
+
config,
|
|
201
|
+
`agent_wallets?id=eq.${encodeURIComponent(id)}&select=${WALLET_SELECT}`,
|
|
202
|
+
{ method: "PATCH", sessionToken: token, body: patch, prefer: "return=representation" }
|
|
203
|
+
);
|
|
204
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
205
|
+
if (!row) throw statusError("Wallet not found on your Apiosk account.", 404);
|
|
206
|
+
return { wallet: toWalletSummary(row) };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export async function hostedDeleteWallet({ env = process.env, sessionToken, walletId } = {}) {
|
|
210
|
+
const config = requireConfig(env);
|
|
211
|
+
const token = requireSession(sessionToken);
|
|
212
|
+
const id = trimString(walletId);
|
|
213
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const rows = await supabaseRest(
|
|
217
|
+
config,
|
|
218
|
+
`agent_wallets?id=eq.${encodeURIComponent(id)}&select=id`,
|
|
219
|
+
{ method: "DELETE", sessionToken: token, prefer: "return=representation" }
|
|
220
|
+
);
|
|
221
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
222
|
+
if (!row) throw statusError("Wallet not found on your Apiosk account.", 404);
|
|
223
|
+
return { deleted: true, wallet_id: id };
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// FK violation → connect tokens still reference the wallet.
|
|
226
|
+
if (error?.status === 409) {
|
|
227
|
+
throw statusError(
|
|
228
|
+
"Revoke the connect tokens that use this wallet first (apiosk_list_wallet_api_keys / apiosk_update_wallet_api_key with revoke=true), then delete it.",
|
|
229
|
+
409
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function hostedWalletActivity({
|
|
237
|
+
env = process.env,
|
|
238
|
+
sessionToken,
|
|
239
|
+
walletId,
|
|
240
|
+
limit = 25,
|
|
241
|
+
} = {}) {
|
|
242
|
+
const config = requireConfig(env);
|
|
243
|
+
const token = requireSession(sessionToken);
|
|
244
|
+
const id = trimString(walletId);
|
|
245
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
246
|
+
|
|
247
|
+
const wallet = await fetchWalletById(config, token, id);
|
|
248
|
+
const cappedLimit = Math.min(Math.max(Number(limit) || 25, 1), 100);
|
|
249
|
+
const rows = await supabaseRest(
|
|
250
|
+
config,
|
|
251
|
+
`gateway_usage?select=id,paid,amount,occurred_at,api_id,endpoint_id,wallet_address,request_id&wallet_address=eq.${encodeURIComponent(
|
|
252
|
+
wallet.address
|
|
253
|
+
)}&order=occurred_at.desc&limit=${cappedLimit}`,
|
|
254
|
+
{ sessionToken: token }
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const calls = Array.isArray(rows)
|
|
258
|
+
? rows.map((row) => ({
|
|
259
|
+
id: row.id,
|
|
260
|
+
paid: Boolean(row.paid),
|
|
261
|
+
amount_usdc: asNumber(row.amount),
|
|
262
|
+
occurred_at: row.occurred_at,
|
|
263
|
+
api_id: row.api_id || null,
|
|
264
|
+
endpoint_id: row.endpoint_id || null,
|
|
265
|
+
request_id: row.request_id || null,
|
|
266
|
+
}))
|
|
267
|
+
: [];
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
wallet,
|
|
271
|
+
calls,
|
|
272
|
+
total_returned: calls.length,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export async function hostedListWalletTokens({ env = process.env, sessionToken, walletId } = {}) {
|
|
277
|
+
const config = requireConfig(env);
|
|
278
|
+
const token = requireSession(sessionToken);
|
|
279
|
+
const id = trimString(walletId);
|
|
280
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
281
|
+
|
|
282
|
+
const rows = await supabaseRest(
|
|
283
|
+
config,
|
|
284
|
+
`agent_wallet_connect_tokens?select=${TOKEN_SELECT}&wallet_id=eq.${encodeURIComponent(
|
|
285
|
+
id
|
|
286
|
+
)}&order=created_at.desc&limit=50`,
|
|
287
|
+
{ sessionToken: token }
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
wallet_id: id,
|
|
292
|
+
keys: (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
293
|
+
id: row.id,
|
|
294
|
+
name: trimString(row.token_name) || "Connect token",
|
|
295
|
+
token_prefix: row.token_prefix,
|
|
296
|
+
expires_at: row.expires_at,
|
|
297
|
+
created_at: row.created_at,
|
|
298
|
+
revoked: Boolean(row.revoked_at),
|
|
299
|
+
})),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function hostedCreateWalletToken({
|
|
304
|
+
env = process.env,
|
|
305
|
+
sessionToken,
|
|
306
|
+
userId,
|
|
307
|
+
walletId,
|
|
308
|
+
name,
|
|
309
|
+
expirationDays,
|
|
310
|
+
revokeExisting = false,
|
|
311
|
+
} = {}) {
|
|
312
|
+
const config = requireConfig(env);
|
|
313
|
+
const token = requireSession(sessionToken);
|
|
314
|
+
const id = trimString(walletId);
|
|
315
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
316
|
+
|
|
317
|
+
const wallet = await fetchWalletById(config, token, id);
|
|
318
|
+
if (!wallet.custodial) {
|
|
319
|
+
throw statusError(
|
|
320
|
+
"This wallet is watch-only (no custodied key), so a connect token minted for it cannot authorize payments.",
|
|
321
|
+
400
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (wallet.status !== "active") {
|
|
325
|
+
throw statusError(`Wallet is not active (status: ${wallet.status}).`, 400);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const uid = await resolveUserId(config, token, userId);
|
|
329
|
+
const ttlDays = Number.isFinite(Number(expirationDays)) && Number(expirationDays) > 0
|
|
330
|
+
? Math.min(Number(expirationDays), 365)
|
|
331
|
+
: DEFAULT_TOKEN_TTL_DAYS;
|
|
332
|
+
const label = trimString(name).slice(0, 64) || "MCP connect token";
|
|
333
|
+
|
|
334
|
+
if (revokeExisting) {
|
|
335
|
+
await supabaseRest(
|
|
336
|
+
config,
|
|
337
|
+
`agent_wallet_connect_tokens?wallet_id=eq.${encodeURIComponent(id)}&revoked_at=is.null`,
|
|
338
|
+
{
|
|
339
|
+
method: "PATCH",
|
|
340
|
+
sessionToken: token,
|
|
341
|
+
body: { revoked_at: new Date().toISOString() },
|
|
342
|
+
}
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const { token: connectToken, tokenHash, tokenPrefix } = generateConnectToken();
|
|
347
|
+
const expiresAt = isoAfterDays(ttlDays);
|
|
348
|
+
const permissions = {
|
|
349
|
+
scope: ["x402.verify", "x402.settle"],
|
|
350
|
+
wallet_ids: [wallet.id],
|
|
351
|
+
wallets: [
|
|
352
|
+
{
|
|
353
|
+
id: wallet.id,
|
|
354
|
+
address: wallet.address,
|
|
355
|
+
daily_limit_usdc: wallet.daily_limit_usdc,
|
|
356
|
+
per_tx_limit_usdc: wallet.per_tx_limit_usdc,
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
include_sepa: false,
|
|
360
|
+
label,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const inserted = await supabaseRest(config, "agent_wallet_connect_tokens", {
|
|
364
|
+
method: "POST",
|
|
365
|
+
sessionToken: token,
|
|
366
|
+
prefer: "return=representation",
|
|
367
|
+
body: {
|
|
368
|
+
wallet_id: wallet.id,
|
|
369
|
+
user_id: uid,
|
|
370
|
+
token_prefix: tokenPrefix,
|
|
371
|
+
token_hash: tokenHash,
|
|
372
|
+
token_name: label,
|
|
373
|
+
permissions,
|
|
374
|
+
include_sepa: false,
|
|
375
|
+
sepa_per_tx_limit_eur: null,
|
|
376
|
+
sepa_daily_limit_eur: null,
|
|
377
|
+
expires_at: expiresAt,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
const row = Array.isArray(inserted) ? inserted[0] : inserted;
|
|
381
|
+
const tokenId = row && trimString(row.id);
|
|
382
|
+
if (!tokenId) throw statusError("Could not create the connect token.", 502);
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await supabaseRest(config, "agent_wallet_connect_token_wallets", {
|
|
386
|
+
method: "POST",
|
|
387
|
+
sessionToken: token,
|
|
388
|
+
body: [{ token_id: tokenId, wallet_id: wallet.id, user_id: uid }],
|
|
389
|
+
});
|
|
390
|
+
} catch (joinError) {
|
|
391
|
+
// Roll back the dangling token so a failed link never leaves an unusable
|
|
392
|
+
// connect token behind (mirrors hosted-payment.mjs / the buyer portal).
|
|
393
|
+
try {
|
|
394
|
+
await supabaseRest(
|
|
395
|
+
config,
|
|
396
|
+
`agent_wallet_connect_tokens?id=eq.${encodeURIComponent(tokenId)}`,
|
|
397
|
+
{
|
|
398
|
+
method: "PATCH",
|
|
399
|
+
sessionToken: token,
|
|
400
|
+
body: { revoked_at: new Date().toISOString() },
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
} catch {
|
|
404
|
+
// Best-effort; the orphan token authorizes no wallet, so it is inert.
|
|
405
|
+
}
|
|
406
|
+
throw joinError;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
key_id: tokenId,
|
|
411
|
+
name: label,
|
|
412
|
+
wallet: { id: wallet.id, address: wallet.address },
|
|
413
|
+
connect_token: connectToken,
|
|
414
|
+
token_prefix: tokenPrefix,
|
|
415
|
+
expires_at: expiresAt,
|
|
416
|
+
warning:
|
|
417
|
+
"This is the only time the full connect token is shown. Store it securely; anyone holding it can spend from this wallet within its limits.",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
export async function hostedUpdateWalletToken({
|
|
422
|
+
env = process.env,
|
|
423
|
+
sessionToken,
|
|
424
|
+
walletId,
|
|
425
|
+
keyId,
|
|
426
|
+
name,
|
|
427
|
+
expirationDays,
|
|
428
|
+
revoke,
|
|
429
|
+
} = {}) {
|
|
430
|
+
const config = requireConfig(env);
|
|
431
|
+
const token = requireSession(sessionToken);
|
|
432
|
+
const id = trimString(walletId);
|
|
433
|
+
const key = trimString(keyId);
|
|
434
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
435
|
+
if (!key) throw statusError("Missing required field: key_id", 400);
|
|
436
|
+
|
|
437
|
+
const patch = {};
|
|
438
|
+
if (name !== undefined) {
|
|
439
|
+
const trimmed = trimString(name).slice(0, 64);
|
|
440
|
+
if (!trimmed) throw statusError("Key name must be 1 to 64 characters.", 400);
|
|
441
|
+
patch.token_name = trimmed;
|
|
442
|
+
}
|
|
443
|
+
if (expirationDays !== undefined) {
|
|
444
|
+
const days = Number(expirationDays);
|
|
445
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
446
|
+
throw statusError("expiration_days must be a positive number.", 400);
|
|
447
|
+
}
|
|
448
|
+
patch.expires_at = isoAfterDays(Math.min(days, 365));
|
|
449
|
+
}
|
|
450
|
+
if (revoke === true) {
|
|
451
|
+
patch.revoked_at = new Date().toISOString();
|
|
452
|
+
}
|
|
453
|
+
if (Object.keys(patch).length === 0) throw statusError("Nothing to update.", 400);
|
|
454
|
+
|
|
455
|
+
const rows = await supabaseRest(
|
|
456
|
+
config,
|
|
457
|
+
`agent_wallet_connect_tokens?id=eq.${encodeURIComponent(key)}&wallet_id=eq.${encodeURIComponent(
|
|
458
|
+
id
|
|
459
|
+
)}&select=${TOKEN_SELECT}`,
|
|
460
|
+
{ method: "PATCH", sessionToken: token, body: patch, prefer: "return=representation" }
|
|
461
|
+
);
|
|
462
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
463
|
+
if (!row) throw statusError("Connect token not found for this wallet.", 404);
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
key: {
|
|
467
|
+
id: row.id,
|
|
468
|
+
name: trimString(row.token_name) || "Connect token",
|
|
469
|
+
token_prefix: row.token_prefix,
|
|
470
|
+
expires_at: row.expires_at,
|
|
471
|
+
created_at: row.created_at,
|
|
472
|
+
revoked: Boolean(row.revoked_at),
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export async function hostedDeleteWalletToken({
|
|
478
|
+
env = process.env,
|
|
479
|
+
sessionToken,
|
|
480
|
+
walletId,
|
|
481
|
+
keyId,
|
|
482
|
+
} = {}) {
|
|
483
|
+
const config = requireConfig(env);
|
|
484
|
+
const token = requireSession(sessionToken);
|
|
485
|
+
const id = trimString(walletId);
|
|
486
|
+
const key = trimString(keyId);
|
|
487
|
+
if (!id) throw statusError("Missing required field: wallet_id", 400);
|
|
488
|
+
if (!key) throw statusError("Missing required field: key_id", 400);
|
|
489
|
+
|
|
490
|
+
// Remove the wallet links first (FK), then the token row itself. If RLS
|
|
491
|
+
// forbids the hard delete, fall back to a revoke so the key is still dead.
|
|
492
|
+
try {
|
|
493
|
+
await supabaseRest(
|
|
494
|
+
config,
|
|
495
|
+
`agent_wallet_connect_token_wallets?token_id=eq.${encodeURIComponent(key)}`,
|
|
496
|
+
{ method: "DELETE", sessionToken: token }
|
|
497
|
+
);
|
|
498
|
+
const rows = await supabaseRest(
|
|
499
|
+
config,
|
|
500
|
+
`agent_wallet_connect_tokens?id=eq.${encodeURIComponent(key)}&wallet_id=eq.${encodeURIComponent(id)}&select=id`,
|
|
501
|
+
{ method: "DELETE", sessionToken: token, prefer: "return=representation" }
|
|
502
|
+
);
|
|
503
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
504
|
+
if (!row) throw statusError("Connect token not found for this wallet.", 404);
|
|
505
|
+
return { deleted: true, key_id: key };
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (error?.status === 404) throw error;
|
|
508
|
+
const revoked = await hostedUpdateWalletToken({
|
|
509
|
+
env,
|
|
510
|
+
sessionToken: token,
|
|
511
|
+
walletId: id,
|
|
512
|
+
keyId: key,
|
|
513
|
+
revoke: true,
|
|
514
|
+
});
|
|
515
|
+
return { deleted: false, revoked: true, key: revoked.key };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function hostedCreateWalletUnavailable() {
|
|
520
|
+
return {
|
|
521
|
+
error: "custodial_wallet_creation_unavailable",
|
|
522
|
+
message:
|
|
523
|
+
"Creating a new managed (custodial) wallet is temporarily unavailable on the hosted MCP server: the legacy dashboard service that derived and encrypted wallet keys has been retired.",
|
|
524
|
+
what_you_can_do: [
|
|
525
|
+
"If your account already has a managed wallet, fund it with USDC on Base mainnet and paid calls will settle automatically (see apiosk_list_wallets).",
|
|
526
|
+
"Sign in to the Apiosk buyer portal to manage wallets and connections.",
|
|
527
|
+
"For fully autonomous local use, run the stdio package (npx -y @apiosk/mcp) and create a local wallet with apiosk_wallet_create.",
|
|
528
|
+
],
|
|
529
|
+
};
|
|
530
|
+
}
|