@waiaas/daemon 2.10.0 → 2.10.1-rc

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 (95) hide show
  1. package/dist/api/helpers/resolve-chain-id.d.ts +2 -0
  2. package/dist/api/helpers/resolve-chain-id.d.ts.map +1 -0
  3. package/dist/api/helpers/resolve-chain-id.js +21 -0
  4. package/dist/api/helpers/resolve-chain-id.js.map +1 -0
  5. package/dist/api/routes/actions.d.ts.map +1 -1
  6. package/dist/api/routes/actions.js +1 -20
  7. package/dist/api/routes/actions.js.map +1 -1
  8. package/dist/api/routes/admin-actions.d.ts.map +1 -1
  9. package/dist/api/routes/admin-actions.js +1 -19
  10. package/dist/api/routes/admin-actions.js.map +1 -1
  11. package/dist/api/routes/admin-auth.d.ts +9 -0
  12. package/dist/api/routes/admin-auth.d.ts.map +1 -0
  13. package/dist/api/routes/admin-auth.js +453 -0
  14. package/dist/api/routes/admin-auth.js.map +1 -0
  15. package/dist/api/routes/admin-monitoring.d.ts +10 -0
  16. package/dist/api/routes/admin-monitoring.d.ts.map +1 -0
  17. package/dist/api/routes/admin-monitoring.js +788 -0
  18. package/dist/api/routes/admin-monitoring.js.map +1 -0
  19. package/dist/api/routes/admin-notifications.d.ts +9 -0
  20. package/dist/api/routes/admin-notifications.d.ts.map +1 -0
  21. package/dist/api/routes/admin-notifications.js +210 -0
  22. package/dist/api/routes/admin-notifications.js.map +1 -0
  23. package/dist/api/routes/admin-settings.d.ts +9 -0
  24. package/dist/api/routes/admin-settings.d.ts.map +1 -0
  25. package/dist/api/routes/admin-settings.js +433 -0
  26. package/dist/api/routes/admin-settings.js.map +1 -0
  27. package/dist/api/routes/admin-wallets.d.ts +16 -0
  28. package/dist/api/routes/admin-wallets.d.ts.map +1 -0
  29. package/dist/api/routes/admin-wallets.js +582 -0
  30. package/dist/api/routes/admin-wallets.js.map +1 -0
  31. package/dist/api/routes/admin.d.ts +8 -26
  32. package/dist/api/routes/admin.d.ts.map +1 -1
  33. package/dist/api/routes/admin.js +20 -2536
  34. package/dist/api/routes/admin.js.map +1 -1
  35. package/dist/api/routes/erc8004.d.ts.map +1 -1
  36. package/dist/api/routes/erc8004.js +0 -1
  37. package/dist/api/routes/erc8004.js.map +1 -1
  38. package/dist/api/routes/mcp.d.ts +2 -0
  39. package/dist/api/routes/mcp.d.ts.map +1 -1
  40. package/dist/api/routes/mcp.js +2 -1
  41. package/dist/api/routes/mcp.js.map +1 -1
  42. package/dist/api/routes/nft-approvals.js +4 -4
  43. package/dist/api/routes/nft-approvals.js.map +1 -1
  44. package/dist/api/routes/sessions.d.ts +2 -0
  45. package/dist/api/routes/sessions.d.ts.map +1 -1
  46. package/dist/api/routes/sessions.js +5 -3
  47. package/dist/api/routes/sessions.js.map +1 -1
  48. package/dist/api/routes/wallets.d.ts.map +1 -1
  49. package/dist/api/routes/wallets.js +3 -3
  50. package/dist/api/routes/wallets.js.map +1 -1
  51. package/dist/api/server.d.ts.map +1 -1
  52. package/dist/api/server.js +2 -0
  53. package/dist/api/server.js.map +1 -1
  54. package/dist/constants.d.ts +17 -0
  55. package/dist/constants.d.ts.map +1 -0
  56. package/dist/constants.js +17 -0
  57. package/dist/constants.js.map +1 -0
  58. package/dist/infrastructure/nft/nft-indexer-client.d.ts.map +1 -1
  59. package/dist/infrastructure/nft/nft-indexer-client.js +3 -3
  60. package/dist/infrastructure/nft/nft-indexer-client.js.map +1 -1
  61. package/dist/infrastructure/oracle/coingecko-forex.d.ts.map +1 -1
  62. package/dist/infrastructure/oracle/coingecko-forex.js +2 -3
  63. package/dist/infrastructure/oracle/coingecko-forex.js.map +1 -1
  64. package/dist/infrastructure/oracle/coingecko-oracle.d.ts.map +1 -1
  65. package/dist/infrastructure/oracle/coingecko-oracle.js +2 -3
  66. package/dist/infrastructure/oracle/coingecko-oracle.js.map +1 -1
  67. package/dist/infrastructure/oracle/pyth-oracle.d.ts +0 -1
  68. package/dist/infrastructure/oracle/pyth-oracle.d.ts.map +1 -1
  69. package/dist/infrastructure/oracle/pyth-oracle.js +3 -3
  70. package/dist/infrastructure/oracle/pyth-oracle.js.map +1 -1
  71. package/dist/lifecycle/workers.d.ts.map +1 -1
  72. package/dist/lifecycle/workers.js +2 -1
  73. package/dist/lifecycle/workers.js.map +1 -1
  74. package/dist/pipeline/dry-run.d.ts.map +1 -1
  75. package/dist/pipeline/dry-run.js +4 -3
  76. package/dist/pipeline/dry-run.js.map +1 -1
  77. package/dist/pipeline/stages.d.ts.map +1 -1
  78. package/dist/pipeline/stages.js +4 -3
  79. package/dist/pipeline/stages.js.map +1 -1
  80. package/dist/services/signing-sdk/channels/ntfy-signing-channel.js +2 -2
  81. package/dist/services/signing-sdk/channels/ntfy-signing-channel.js.map +1 -1
  82. package/dist/services/signing-sdk/channels/telegram-signing-channel.js +1 -1
  83. package/dist/services/signing-sdk/channels/telegram-signing-channel.js.map +1 -1
  84. package/dist/services/signing-sdk/channels/wallet-notification-channel.js +2 -2
  85. package/dist/services/signing-sdk/channels/wallet-notification-channel.js.map +1 -1
  86. package/dist/services/signing-sdk/sign-request-builder.d.ts +2 -0
  87. package/dist/services/signing-sdk/sign-request-builder.d.ts.map +1 -1
  88. package/dist/services/signing-sdk/sign-request-builder.js +20 -3
  89. package/dist/services/signing-sdk/sign-request-builder.js.map +1 -1
  90. package/dist/services/signing-sdk/sign-response-handler.js +4 -4
  91. package/dist/services/signing-sdk/sign-response-handler.js.map +1 -1
  92. package/package.json +5 -5
  93. package/public/admin/assets/index-By5VUJ-B.js +3 -0
  94. package/public/admin/index.html +1 -1
  95. package/public/admin/assets/index--wQVT9Dz.js +0 -3
@@ -0,0 +1,788 @@
1
+ /**
2
+ * Admin Monitoring route handlers: transactions, incoming, agent-prompt, session-reissue,
3
+ * tx-cancel, tx-reject, backup, stats, autostop.
4
+ *
5
+ * Extracted from admin.ts for maintainability.
6
+ */
7
+ import { createRoute, z } from '@hono/zod-openapi';
8
+ import { sql, desc, eq, and, isNull, gt, count as drizzleCount } from 'drizzle-orm';
9
+ import { createHash } from 'node:crypto';
10
+ import { WAIaaSError, getNetworksForEnvironment } from '@waiaas/core';
11
+ import { wallets, sessions, sessionWallets, policies, transactions, incomingTransactions } from '../../infrastructure/database/schema.js';
12
+ import { generateId } from '../../infrastructure/database/id.js';
13
+ import { buildConnectInfoPrompt } from './connect-info.js';
14
+ import { AgentPromptRequestSchema, AgentPromptResponseSchema, SessionReissueResponseSchema, BackupInfoResponseSchema, BackupListResponseSchema, ErrorResponseSchema, buildErrorResponses, } from './openapi-schemas.js';
15
+ import { formatTxAmount } from './admin-wallets.js';
16
+ // ---------------------------------------------------------------------------
17
+ // Route definitions (top-level)
18
+ // ---------------------------------------------------------------------------
19
+ const adminTransactionsQuerySchema = z.object({
20
+ offset: z.coerce.number().int().min(0).default(0).optional(),
21
+ limit: z.coerce.number().int().min(1).max(100).default(20).optional(),
22
+ wallet_id: z.string().uuid().optional(),
23
+ type: z.string().optional(),
24
+ status: z.string().optional(),
25
+ network: z.string().optional(),
26
+ since: z.coerce.number().optional(),
27
+ until: z.coerce.number().optional(),
28
+ search: z.string().optional(),
29
+ });
30
+ const adminTransactionsRoute = createRoute({
31
+ method: 'get',
32
+ path: '/admin/transactions',
33
+ tags: ['Admin'],
34
+ summary: 'List cross-wallet transactions with filters and pagination',
35
+ request: {
36
+ query: adminTransactionsQuerySchema,
37
+ },
38
+ responses: {
39
+ 200: {
40
+ description: 'Paginated cross-wallet transaction list',
41
+ content: {
42
+ 'application/json': {
43
+ schema: z.object({
44
+ items: z.array(z.object({
45
+ id: z.string(),
46
+ walletId: z.string(),
47
+ walletName: z.string().nullable(),
48
+ type: z.string(),
49
+ status: z.string(),
50
+ tier: z.string().nullable(),
51
+ toAddress: z.string().nullable(),
52
+ amount: z.string().nullable(),
53
+ amountUsd: z.number().nullable(),
54
+ network: z.string().nullable(),
55
+ txHash: z.string().nullable(),
56
+ chain: z.string(),
57
+ createdAt: z.number().nullable(),
58
+ })),
59
+ total: z.number().int(),
60
+ offset: z.number().int(),
61
+ limit: z.number().int(),
62
+ }),
63
+ },
64
+ },
65
+ },
66
+ },
67
+ });
68
+ const adminIncomingQuerySchema = z.object({
69
+ offset: z.coerce.number().int().min(0).default(0).optional(),
70
+ limit: z.coerce.number().int().min(1).max(100).default(20).optional(),
71
+ wallet_id: z.string().uuid().optional(),
72
+ chain: z.string().optional(),
73
+ status: z.string().optional(),
74
+ suspicious: z.enum(['true', 'false']).optional(),
75
+ });
76
+ const adminIncomingRoute = createRoute({
77
+ method: 'get',
78
+ path: '/admin/incoming',
79
+ tags: ['Admin'],
80
+ summary: 'List cross-wallet incoming transactions with filters and pagination',
81
+ request: {
82
+ query: adminIncomingQuerySchema,
83
+ },
84
+ responses: {
85
+ 200: {
86
+ description: 'Paginated cross-wallet incoming transaction list',
87
+ content: {
88
+ 'application/json': {
89
+ schema: z.object({
90
+ items: z.array(z.object({
91
+ id: z.string(),
92
+ txHash: z.string(),
93
+ walletId: z.string(),
94
+ walletName: z.string().nullable(),
95
+ fromAddress: z.string(),
96
+ amount: z.string(),
97
+ tokenAddress: z.string().nullable(),
98
+ chain: z.string(),
99
+ network: z.string(),
100
+ status: z.string(),
101
+ blockNumber: z.number().nullable(),
102
+ detectedAt: z.number().nullable(),
103
+ confirmedAt: z.number().nullable(),
104
+ suspicious: z.boolean(),
105
+ })),
106
+ total: z.number().int(),
107
+ offset: z.number().int(),
108
+ limit: z.number().int(),
109
+ }),
110
+ },
111
+ },
112
+ },
113
+ },
114
+ });
115
+ // ---------------------------------------------------------------------------
116
+ // Register handlers
117
+ // ---------------------------------------------------------------------------
118
+ export function registerAdminMonitoringRoutes(router, deps) {
119
+ // GET /admin/transactions (cross-wallet transaction list)
120
+ router.openapi(adminTransactionsRoute, async (c) => {
121
+ const query = c.req.valid('query');
122
+ const offset = query.offset ?? 0;
123
+ const limit = query.limit ?? 20;
124
+ // Build WHERE conditions
125
+ const conditions = [];
126
+ if (query.wallet_id) {
127
+ conditions.push(eq(transactions.walletId, query.wallet_id));
128
+ }
129
+ if (query.type) {
130
+ conditions.push(eq(transactions.type, query.type));
131
+ }
132
+ if (query.status) {
133
+ conditions.push(eq(transactions.status, query.status));
134
+ }
135
+ if (query.network) {
136
+ conditions.push(eq(transactions.network, query.network));
137
+ }
138
+ if (query.since !== undefined) {
139
+ conditions.push(sql `${transactions.createdAt} >= ${query.since}`);
140
+ }
141
+ if (query.until !== undefined) {
142
+ conditions.push(sql `${transactions.createdAt} <= ${query.until}`);
143
+ }
144
+ if (query.search) {
145
+ const pattern = `%${query.search}%`;
146
+ conditions.push(sql `(${transactions.txHash} LIKE ${pattern} OR ${transactions.toAddress} LIKE ${pattern})`);
147
+ }
148
+ const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
149
+ // Count total
150
+ const totalResult = deps.db
151
+ .select({ count: drizzleCount() })
152
+ .from(transactions)
153
+ .where(whereClause)
154
+ .get();
155
+ const total = totalResult?.count ?? 0;
156
+ // Query with JOIN for walletName
157
+ const rows = deps.db
158
+ .select({
159
+ id: transactions.id,
160
+ walletId: transactions.walletId,
161
+ walletName: wallets.name,
162
+ type: transactions.type,
163
+ status: transactions.status,
164
+ tier: transactions.tier,
165
+ toAddress: transactions.toAddress,
166
+ amount: transactions.amount,
167
+ amountUsd: transactions.amountUsd,
168
+ network: transactions.network,
169
+ txHash: transactions.txHash,
170
+ chain: transactions.chain,
171
+ createdAt: transactions.createdAt,
172
+ tokenMint: transactions.tokenMint,
173
+ contractAddress: transactions.contractAddress,
174
+ })
175
+ .from(transactions)
176
+ .leftJoin(wallets, eq(transactions.walletId, wallets.id))
177
+ .where(whereClause)
178
+ .orderBy(desc(transactions.createdAt))
179
+ .offset(offset)
180
+ .limit(limit)
181
+ .all();
182
+ const items = rows.map((row) => {
183
+ const tokenAddr = row.tokenMint ?? row.contractAddress ?? null;
184
+ return {
185
+ id: row.id,
186
+ walletId: row.walletId,
187
+ walletName: row.walletName ?? null,
188
+ type: row.type,
189
+ status: row.status,
190
+ tier: row.tier ?? null,
191
+ toAddress: row.toAddress ?? null,
192
+ amount: row.amount ?? null,
193
+ formattedAmount: formatTxAmount(row.amount ?? null, row.chain, row.network ?? null, tokenAddr, deps.db),
194
+ amountUsd: row.amountUsd ?? null,
195
+ network: row.network ?? null,
196
+ txHash: row.txHash ?? null,
197
+ chain: row.chain,
198
+ createdAt: row.createdAt instanceof Date
199
+ ? Math.floor(row.createdAt.getTime() / 1000)
200
+ : (typeof row.createdAt === 'number' ? row.createdAt : null),
201
+ };
202
+ });
203
+ return c.json({ items, total, offset, limit }, 200);
204
+ });
205
+ // GET /admin/incoming (cross-wallet incoming transaction list)
206
+ router.openapi(adminIncomingRoute, async (c) => {
207
+ const query = c.req.valid('query');
208
+ const offset = query.offset ?? 0;
209
+ const limit = query.limit ?? 20;
210
+ // Build WHERE conditions (no default status filter -- admin sees all)
211
+ const conditions = [];
212
+ if (query.wallet_id) {
213
+ conditions.push(eq(incomingTransactions.walletId, query.wallet_id));
214
+ }
215
+ if (query.chain) {
216
+ conditions.push(eq(incomingTransactions.chain, query.chain));
217
+ }
218
+ if (query.status) {
219
+ conditions.push(eq(incomingTransactions.status, query.status));
220
+ }
221
+ if (query.suspicious !== undefined) {
222
+ conditions.push(eq(incomingTransactions.isSuspicious, query.suspicious === 'true'));
223
+ }
224
+ const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
225
+ // Count total
226
+ const totalResult = deps.db
227
+ .select({ count: drizzleCount() })
228
+ .from(incomingTransactions)
229
+ .where(whereClause)
230
+ .get();
231
+ const total = totalResult?.count ?? 0;
232
+ // Query with JOIN for walletName
233
+ const rows = deps.db
234
+ .select({
235
+ id: incomingTransactions.id,
236
+ txHash: incomingTransactions.txHash,
237
+ walletId: incomingTransactions.walletId,
238
+ walletName: wallets.name,
239
+ fromAddress: incomingTransactions.fromAddress,
240
+ amount: incomingTransactions.amount,
241
+ tokenAddress: incomingTransactions.tokenAddress,
242
+ chain: incomingTransactions.chain,
243
+ network: incomingTransactions.network,
244
+ status: incomingTransactions.status,
245
+ blockNumber: incomingTransactions.blockNumber,
246
+ detectedAt: incomingTransactions.detectedAt,
247
+ confirmedAt: incomingTransactions.confirmedAt,
248
+ isSuspicious: incomingTransactions.isSuspicious,
249
+ })
250
+ .from(incomingTransactions)
251
+ .leftJoin(wallets, eq(incomingTransactions.walletId, wallets.id))
252
+ .where(whereClause)
253
+ .orderBy(desc(incomingTransactions.detectedAt))
254
+ .offset(offset)
255
+ .limit(limit)
256
+ .all();
257
+ const items = rows.map((row) => ({
258
+ id: row.id,
259
+ txHash: row.txHash,
260
+ walletId: row.walletId,
261
+ walletName: row.walletName ?? null,
262
+ fromAddress: row.fromAddress,
263
+ amount: row.amount,
264
+ formattedAmount: formatTxAmount(row.amount, row.chain, row.network, row.tokenAddress ?? null, deps.db),
265
+ tokenAddress: row.tokenAddress ?? null,
266
+ chain: row.chain,
267
+ network: row.network,
268
+ status: row.status,
269
+ blockNumber: row.blockNumber ?? null,
270
+ detectedAt: row.detectedAt instanceof Date
271
+ ? Math.floor(row.detectedAt.getTime() / 1000)
272
+ : (typeof row.detectedAt === 'number' ? row.detectedAt : null),
273
+ confirmedAt: row.confirmedAt instanceof Date
274
+ ? Math.floor(row.confirmedAt.getTime() / 1000)
275
+ : (typeof row.confirmedAt === 'number' ? row.confirmedAt : null),
276
+ suspicious: row.isSuspicious ?? false,
277
+ }));
278
+ return c.json({ items, total, offset, limit }, 200);
279
+ });
280
+ // POST /admin/agent-prompt -- Generate agent connection prompt
281
+ const agentPromptRoute = createRoute({
282
+ method: 'post',
283
+ path: '/admin/agent-prompt',
284
+ tags: ['Admin'],
285
+ summary: 'Generate agent connection prompt (magic word)',
286
+ request: {
287
+ body: {
288
+ content: { 'application/json': { schema: AgentPromptRequestSchema } },
289
+ },
290
+ },
291
+ responses: {
292
+ 201: {
293
+ description: 'Agent prompt generated',
294
+ content: { 'application/json': { schema: AgentPromptResponseSchema } },
295
+ },
296
+ ...buildErrorResponses(['ADAPTER_NOT_AVAILABLE']),
297
+ },
298
+ });
299
+ router.openapi(agentPromptRoute, async (c) => {
300
+ if (!deps.jwtSecretManager || !deps.daemonConfig) {
301
+ throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', { message: 'JWT signing not available' });
302
+ }
303
+ const body = c.req.valid('json');
304
+ const nowSec = Math.floor(Date.now() / 1000);
305
+ // v29.9: per-session TTL; omit = unlimited
306
+ const ttl = body.ttl; // undefined = unlimited session
307
+ const expiresAt = ttl !== undefined ? nowSec + ttl : 0; // 0 = unlimited
308
+ // Get target wallets (with environment for prompt builder)
309
+ let targetWallets;
310
+ if (body.walletIds && body.walletIds.length > 0) {
311
+ targetWallets = body.walletIds
312
+ .map((wid) => deps.db.select().from(wallets).where(eq(wallets.id, wid)).get())
313
+ .filter((w) => w != null && w.status === 'ACTIVE')
314
+ .map((w) => ({ id: w.id, name: w.name, chain: w.chain, environment: w.environment, publicKey: w.publicKey }));
315
+ }
316
+ else {
317
+ targetWallets = deps.db
318
+ .select()
319
+ .from(wallets)
320
+ .where(eq(wallets.status, 'ACTIVE'))
321
+ .all()
322
+ .map((w) => ({ id: w.id, name: w.name, chain: w.chain, environment: w.environment, publicKey: w.publicKey }));
323
+ }
324
+ if (targetWallets.length === 0) {
325
+ return c.json({ prompt: '', walletCount: 0, sessionsCreated: 0, sessionReused: false, expiresAt }, 201);
326
+ }
327
+ // Try to reuse an existing valid session covering all target wallets
328
+ const defaultWallet = targetWallets[0];
329
+ const targetWalletIds = targetWallets.map((w) => w.id);
330
+ let sessionId;
331
+ let sessionReused = false;
332
+ let sessionsCreated = 1;
333
+ let actualExpiresAt = expiresAt;
334
+ // Find active sessions that cover all target wallets
335
+ const candidateSessions = deps.db
336
+ .select({
337
+ id: sessions.id,
338
+ expiresAt: sessions.expiresAt,
339
+ })
340
+ .from(sessions)
341
+ .where(and(isNull(sessions.revokedAt), ttl !== undefined
342
+ ? gt(sessions.expiresAt, new Date((nowSec + Math.max(Math.floor(ttl * 0.1), 3600)) * 1000))
343
+ : sql `(${sessions.expiresAt} = 0 OR ${sessions.expiresAt} > ${nowSec})`))
344
+ .all();
345
+ let reusableSessionId = null;
346
+ let reusableExpiresAt = 0;
347
+ for (const candidate of candidateSessions) {
348
+ // Count how many of our target wallets are linked to this session
349
+ const linkedCount = deps.db
350
+ .select({ cnt: drizzleCount() })
351
+ .from(sessionWallets)
352
+ .where(and(eq(sessionWallets.sessionId, candidate.id), sql `${sessionWallets.walletId} IN (${sql.join(targetWalletIds.map((id) => sql `${id}`), sql `, `)})`))
353
+ .get();
354
+ if (linkedCount && linkedCount.cnt === targetWalletIds.length) {
355
+ reusableSessionId = candidate.id;
356
+ reusableExpiresAt = candidate.expiresAt instanceof Date
357
+ ? Math.floor(candidate.expiresAt.getTime() / 1000)
358
+ : candidate.expiresAt;
359
+ break;
360
+ }
361
+ }
362
+ if (reusableSessionId) {
363
+ // Reuse existing session
364
+ sessionId = reusableSessionId;
365
+ sessionReused = true;
366
+ sessionsCreated = 0;
367
+ actualExpiresAt = reusableExpiresAt;
368
+ }
369
+ else {
370
+ // Create a new multi-wallet session
371
+ sessionId = generateId();
372
+ deps.db.insert(sessions).values({
373
+ id: sessionId,
374
+ tokenHash: '',
375
+ expiresAt: new Date(expiresAt * 1000),
376
+ absoluteExpiresAt: new Date(0), // unlimited
377
+ createdAt: new Date(nowSec * 1000),
378
+ renewalCount: 0,
379
+ maxRenewals: 0, // unlimited
380
+ constraints: null,
381
+ source: 'api',
382
+ }).run();
383
+ // Insert N rows into session_wallets
384
+ for (let i = 0; i < targetWallets.length; i++) {
385
+ const w = targetWallets[i];
386
+ deps.db.insert(sessionWallets).values({
387
+ sessionId,
388
+ walletId: w.id,
389
+ createdAt: new Date(nowSec * 1000),
390
+ }).run();
391
+ }
392
+ void deps.notificationService?.notify('SESSION_CREATED', defaultWallet.id, { sessionId });
393
+ }
394
+ // Sign JWT
395
+ const jwtPayload = {
396
+ sub: sessionId,
397
+ iat: nowSec,
398
+ exp: actualExpiresAt > 0 ? actualExpiresAt : undefined,
399
+ };
400
+ const token = await deps.jwtSecretManager.signToken(jwtPayload);
401
+ if (!sessionReused) {
402
+ const tokenHash = createHash('sha256').update(token).digest('hex');
403
+ deps.db.update(sessions).set({ tokenHash }).where(eq(sessions.id, sessionId)).run();
404
+ }
405
+ // Query per-wallet policies for prompt builder
406
+ const promptWallets = targetWallets.map((w) => {
407
+ const walletPolicies = deps.db
408
+ .select({ type: policies.type })
409
+ .from(policies)
410
+ .where(and(eq(policies.walletId, w.id), eq(policies.enabled, true)))
411
+ .all();
412
+ const networks = getNetworksForEnvironment(w.chain, w.environment);
413
+ return {
414
+ id: w.id,
415
+ name: w.name,
416
+ chain: w.chain,
417
+ environment: w.environment,
418
+ address: w.publicKey,
419
+ networks: networks.map((n) => n),
420
+ policies: walletPolicies,
421
+ };
422
+ });
423
+ // Compute capabilities dynamically (same logic as connect-info)
424
+ const capabilities = ['transfer', 'token_transfer', 'balance', 'assets'];
425
+ if (deps.settingsService) {
426
+ try {
427
+ if (deps.settingsService.get('signing_sdk.enabled') === 'true') {
428
+ capabilities.push('sign');
429
+ }
430
+ }
431
+ catch {
432
+ // Setting key not found -- signing not available
433
+ }
434
+ }
435
+ if (deps.settingsService && deps.actionProviderRegistry) {
436
+ try {
437
+ const providers = deps.actionProviderRegistry.listProviders();
438
+ const hasAnyKey = providers.some((p) => p.requiresApiKey && deps.settingsService.hasApiKey(p.name));
439
+ if (hasAnyKey) {
440
+ capabilities.push('actions');
441
+ }
442
+ }
443
+ catch {
444
+ // Settings service not available
445
+ }
446
+ }
447
+ if (deps.daemonConfig?.x402?.enabled === true) {
448
+ capabilities.push('x402');
449
+ }
450
+ // Read default-deny toggles
451
+ const defaultDeny = {
452
+ tokenTransfers: deps.settingsService?.get('policy.default_deny_tokens') !== 'false',
453
+ contractCalls: deps.settingsService?.get('policy.default_deny_contracts') !== 'false',
454
+ tokenApprovals: deps.settingsService?.get('policy.default_deny_spenders') !== 'false',
455
+ x402Domains: deps.settingsService?.get('policy.default_deny_x402_domains') !== 'false',
456
+ };
457
+ // Build prompt using shared prompt builder
458
+ const host = c.req.header('Host') ?? 'localhost:3100';
459
+ const protocol = c.req.header('X-Forwarded-Proto') ?? 'http';
460
+ const baseUrl = `${protocol}://${host}`;
461
+ const prompt = buildConnectInfoPrompt({
462
+ wallets: promptWallets,
463
+ capabilities,
464
+ defaultDeny,
465
+ baseUrl,
466
+ version: deps.version,
467
+ });
468
+ // Append session token so the agent can start using it immediately
469
+ const fullPrompt = `${prompt}\n\nSession Token: ${token}\nSession ID: ${sessionId}`;
470
+ return c.json({
471
+ prompt: fullPrompt,
472
+ walletCount: targetWallets.length,
473
+ sessionsCreated,
474
+ sessionReused,
475
+ expiresAt: actualExpiresAt,
476
+ }, 201);
477
+ });
478
+ // POST /admin/sessions/:id/reissue -- Reissue session token
479
+ const sessionReissueRoute = createRoute({
480
+ method: 'post',
481
+ path: '/admin/sessions/{id}/reissue',
482
+ tags: ['Admin'],
483
+ summary: 'Reissue session token (re-sign JWT for existing session)',
484
+ request: {
485
+ params: z.object({ id: z.string().uuid() }),
486
+ },
487
+ responses: {
488
+ 200: {
489
+ description: 'Token reissued',
490
+ content: { 'application/json': { schema: SessionReissueResponseSchema } },
491
+ },
492
+ ...buildErrorResponses(['SESSION_NOT_FOUND', 'SESSION_REVOKED']),
493
+ },
494
+ });
495
+ router.openapi(sessionReissueRoute, async (c) => {
496
+ if (!deps.jwtSecretManager) {
497
+ throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', { message: 'JWT signing not available' });
498
+ }
499
+ const { id: sessionId } = c.req.valid('param');
500
+ const nowSec = Math.floor(Date.now() / 1000);
501
+ // Find session
502
+ const session = deps.db
503
+ .select()
504
+ .from(sessions)
505
+ .where(eq(sessions.id, sessionId))
506
+ .get();
507
+ if (!session) {
508
+ throw new WAIaaSError('SESSION_NOT_FOUND');
509
+ }
510
+ if (session.revokedAt) {
511
+ throw new WAIaaSError('SESSION_REVOKED');
512
+ }
513
+ const expiresAtSec = session.expiresAt instanceof Date
514
+ ? Math.floor(session.expiresAt.getTime() / 1000)
515
+ : session.expiresAt;
516
+ if (expiresAtSec > 0 && expiresAtSec <= nowSec) {
517
+ throw new WAIaaSError('SESSION_NOT_FOUND', { message: 'Session expired' });
518
+ }
519
+ // Re-sign JWT (no wallet claim needed -- walletId resolved at request time)
520
+ const jwtPayload = {
521
+ sub: sessionId,
522
+ iat: nowSec,
523
+ ...(expiresAtSec > 0 ? { exp: expiresAtSec } : {}),
524
+ };
525
+ const token = await deps.jwtSecretManager.signToken(jwtPayload);
526
+ // Increment token_issued_count
527
+ const newCount = (session.tokenIssuedCount ?? 1) + 1;
528
+ deps.db.update(sessions)
529
+ .set({ tokenIssuedCount: newCount })
530
+ .where(eq(sessions.id, sessionId))
531
+ .run();
532
+ return c.json({
533
+ token,
534
+ sessionId,
535
+ tokenIssuedCount: newCount,
536
+ expiresAt: expiresAtSec,
537
+ }, 200);
538
+ });
539
+ // POST /admin/transactions/:id/cancel -- Cancel a QUEUED (DELAY) transaction
540
+ const adminTxCancelRoute = createRoute({
541
+ method: 'post',
542
+ path: '/admin/transactions/{id}/cancel',
543
+ tags: ['Admin'],
544
+ summary: 'Cancel a delayed (QUEUED) transaction',
545
+ request: {
546
+ params: z.object({ id: z.string().uuid() }),
547
+ },
548
+ responses: {
549
+ 200: {
550
+ description: 'Transaction cancelled',
551
+ content: {
552
+ 'application/json': {
553
+ schema: z.object({
554
+ id: z.string(),
555
+ status: z.literal('CANCELLED'),
556
+ }),
557
+ },
558
+ },
559
+ },
560
+ ...buildErrorResponses(['TX_NOT_FOUND']),
561
+ },
562
+ });
563
+ router.openapi(adminTxCancelRoute, async (c) => {
564
+ const { id: txId } = c.req.valid('param');
565
+ if (!deps.delayQueue) {
566
+ throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
567
+ message: 'Delay queue not available',
568
+ });
569
+ }
570
+ deps.delayQueue.cancelDelay(txId);
571
+ return c.json({ id: txId, status: 'CANCELLED' }, 200);
572
+ });
573
+ // POST /admin/transactions/:id/reject -- Reject a pending (APPROVAL) transaction
574
+ const adminTxRejectRoute = createRoute({
575
+ method: 'post',
576
+ path: '/admin/transactions/{id}/reject',
577
+ tags: ['Admin'],
578
+ summary: 'Reject a pending approval transaction',
579
+ request: {
580
+ params: z.object({ id: z.string().uuid() }),
581
+ },
582
+ responses: {
583
+ 200: {
584
+ description: 'Transaction rejected',
585
+ content: {
586
+ 'application/json': {
587
+ schema: z.object({
588
+ id: z.string(),
589
+ status: z.literal('CANCELLED'),
590
+ rejectedAt: z.number(),
591
+ }),
592
+ },
593
+ },
594
+ },
595
+ ...buildErrorResponses(['TX_NOT_FOUND']),
596
+ },
597
+ });
598
+ router.openapi(adminTxRejectRoute, async (c) => {
599
+ const { id: txId } = c.req.valid('param');
600
+ if (!deps.approvalWorkflow) {
601
+ throw new WAIaaSError('ADAPTER_NOT_AVAILABLE', {
602
+ message: 'Approval workflow not available',
603
+ });
604
+ }
605
+ const result = deps.approvalWorkflow.reject(txId);
606
+ return c.json({
607
+ id: txId,
608
+ status: 'CANCELLED',
609
+ rejectedAt: result.rejectedAt,
610
+ }, 200);
611
+ });
612
+ // POST /admin/backup (create encrypted backup)
613
+ const createBackupRoute = createRoute({
614
+ method: 'post',
615
+ path: '/admin/backup',
616
+ tags: ['Admin'],
617
+ summary: 'Create an encrypted backup',
618
+ responses: {
619
+ 200: {
620
+ description: 'Backup created successfully',
621
+ content: { 'application/json': { schema: BackupInfoResponseSchema } },
622
+ },
623
+ 401: {
624
+ description: 'Master password not available',
625
+ content: { 'application/json': { schema: ErrorResponseSchema } },
626
+ },
627
+ 501: {
628
+ description: 'Backup service not configured',
629
+ content: { 'application/json': { schema: ErrorResponseSchema } },
630
+ },
631
+ ...buildErrorResponses(['INVALID_MASTER_PASSWORD']),
632
+ },
633
+ });
634
+ router.openapi(createBackupRoute, async (c) => {
635
+ if (!deps.encryptedBackupService) {
636
+ return c.json({ code: 'NOT_CONFIGURED', message: 'Backup service not configured', retryable: false }, 501);
637
+ }
638
+ if (!deps.passwordRef?.password) {
639
+ return c.json({ code: 'INVALID_MASTER_PASSWORD', message: 'Master password not available', retryable: false }, 401);
640
+ }
641
+ const info = await deps.encryptedBackupService.createBackup(deps.passwordRef.password);
642
+ return c.json(info, 200);
643
+ });
644
+ // GET /admin/backups (list backups)
645
+ const listBackupsRoute = createRoute({
646
+ method: 'get',
647
+ path: '/admin/backups',
648
+ tags: ['Admin'],
649
+ summary: 'List available backups',
650
+ responses: {
651
+ 200: {
652
+ description: 'Backup list',
653
+ content: { 'application/json': { schema: BackupListResponseSchema } },
654
+ },
655
+ 501: {
656
+ description: 'Backup service not configured',
657
+ content: { 'application/json': { schema: ErrorResponseSchema } },
658
+ },
659
+ },
660
+ });
661
+ router.openapi(listBackupsRoute, async (c) => {
662
+ if (!deps.encryptedBackupService) {
663
+ return c.json({ code: 'NOT_CONFIGURED', message: 'Backup service not configured', retryable: false }, 501);
664
+ }
665
+ const backups = deps.encryptedBackupService.listBackups();
666
+ const retentionCount = deps.daemonConfig?.backup?.retention_count ?? 7;
667
+ return c.json({ backups, total: backups.length, retention_count: retentionCount }, 200);
668
+ });
669
+ // GET /admin/stats -- 7-category operational statistics (STAT-01)
670
+ const adminStatsRoute = createRoute({
671
+ method: 'get',
672
+ path: '/admin/stats',
673
+ tags: ['Admin'],
674
+ summary: 'Get operational statistics',
675
+ responses: {
676
+ 200: {
677
+ description: 'Operational statistics (7 categories)',
678
+ content: { 'application/json': { schema: z.any() } },
679
+ },
680
+ },
681
+ });
682
+ router.openapi(adminStatsRoute, async (c) => {
683
+ if (!deps.adminStatsService) {
684
+ throw new WAIaaSError('STATS_NOT_CONFIGURED');
685
+ }
686
+ const stats = deps.adminStatsService.getStats();
687
+ return c.json(stats, 200);
688
+ });
689
+ // GET /admin/autostop/rules -- List AutoStop rules with status (PLUG-03)
690
+ const autostopRulesRoute = createRoute({
691
+ method: 'get',
692
+ path: '/admin/autostop/rules',
693
+ tags: ['Admin'],
694
+ summary: 'List AutoStop rules with status',
695
+ responses: {
696
+ 200: {
697
+ description: 'AutoStop rules list',
698
+ content: { 'application/json': { schema: z.any() } },
699
+ },
700
+ },
701
+ });
702
+ router.openapi(autostopRulesRoute, async (c) => {
703
+ if (!deps.autoStopService) {
704
+ return c.json({ globalEnabled: false, rules: [] }, 200);
705
+ }
706
+ const status = deps.autoStopService.getStatus();
707
+ const registry = deps.autoStopService.registry;
708
+ const rules = registry.getRules().map((r) => {
709
+ const ruleStatus = r.getStatus();
710
+ return {
711
+ id: r.id,
712
+ displayName: r.displayName,
713
+ description: r.description,
714
+ enabled: r.enabled,
715
+ subscribedEvents: r.subscribedEvents,
716
+ config: ruleStatus.config,
717
+ state: ruleStatus.state,
718
+ };
719
+ });
720
+ return c.json({ globalEnabled: status.enabled, rules }, 200);
721
+ });
722
+ // PUT /admin/autostop/rules/:id -- Update AutoStop rule (PLUG-03)
723
+ const autostopRuleUpdateRoute = createRoute({
724
+ method: 'put',
725
+ path: '/admin/autostop/rules/{id}',
726
+ tags: ['Admin'],
727
+ summary: 'Update AutoStop rule enabled/config',
728
+ request: {
729
+ params: z.object({ id: z.string() }),
730
+ body: {
731
+ content: {
732
+ 'application/json': {
733
+ schema: z.object({
734
+ enabled: z.boolean().optional(),
735
+ config: z.record(z.unknown()).optional(),
736
+ }),
737
+ },
738
+ },
739
+ },
740
+ },
741
+ responses: {
742
+ 200: {
743
+ description: 'Rule updated',
744
+ content: { 'application/json': { schema: z.any() } },
745
+ },
746
+ 404: {
747
+ description: 'Rule not found',
748
+ content: { 'application/json': { schema: z.any() } },
749
+ },
750
+ },
751
+ });
752
+ router.openapi(autostopRuleUpdateRoute, async (c) => {
753
+ if (!deps.autoStopService) {
754
+ throw new WAIaaSError('RULE_NOT_FOUND');
755
+ }
756
+ const { id } = c.req.valid('param');
757
+ const body = c.req.valid('json');
758
+ const registry = deps.autoStopService.registry;
759
+ const rule = registry.getRule(id);
760
+ if (!rule) {
761
+ throw new WAIaaSError('RULE_NOT_FOUND');
762
+ }
763
+ // Update enabled state
764
+ if (body.enabled !== undefined) {
765
+ registry.setEnabled(id, body.enabled);
766
+ // Persist to Admin Settings
767
+ if (deps.settingsService) {
768
+ deps.settingsService.set(`autostop.rule.${id}.enabled`, String(body.enabled));
769
+ }
770
+ }
771
+ // Update config
772
+ if (body.config) {
773
+ rule.updateConfig(body.config);
774
+ }
775
+ // Return updated rule info
776
+ const ruleStatus = rule.getStatus();
777
+ return c.json({
778
+ id: rule.id,
779
+ displayName: rule.displayName,
780
+ description: rule.description,
781
+ enabled: rule.enabled,
782
+ subscribedEvents: rule.subscribedEvents,
783
+ config: ruleStatus.config,
784
+ state: ruleStatus.state,
785
+ }, 200);
786
+ });
787
+ }
788
+ //# sourceMappingURL=admin-monitoring.js.map