@hivemind-os/collective-daemon 0.2.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.
@@ -0,0 +1,1005 @@
1
+ import {
2
+ saveConfig
3
+ } from "./chunk-CJHYQ7RR.js";
4
+ import {
5
+ buildOAuthConfig
6
+ } from "./chunk-Q3V4V7UR.js";
7
+
8
+ // src/portal/server.ts
9
+ import { randomBytes } from "crypto";
10
+ import cors from "@fastify/cors";
11
+ import Fastify from "fastify";
12
+ import { createPkcePair } from "@hivemind-os/collective-core";
13
+ var PENDING_AUTH_TTL_MS = 10 * 60 * 1e3;
14
+ var PortalServer = class {
15
+ constructor(options) {
16
+ this.options = options;
17
+ this.server = Fastify({ logger: false });
18
+ this.setupComplete = Boolean(options.state);
19
+ this.completionPromise = new Promise((resolvePromise) => {
20
+ this.resolveCompletion = resolvePromise;
21
+ });
22
+ }
23
+ options;
24
+ server;
25
+ pendingAuth = /* @__PURE__ */ new Map();
26
+ baseUrl = "";
27
+ setupComplete = false;
28
+ completionPromise;
29
+ resolveCompletion;
30
+ async start() {
31
+ this.server.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "string" }, (_request, body, done) => {
32
+ const payload = typeof body === "string" ? body : body.toString("utf8");
33
+ done(null, Object.fromEntries(new URLSearchParams(payload)));
34
+ });
35
+ await this.server.register(cors, {
36
+ origin: (origin, callback) => {
37
+ if (!origin) {
38
+ callback(null, true);
39
+ return;
40
+ }
41
+ callback(null, isLoopbackOrigin(origin));
42
+ }
43
+ });
44
+ this.registerRoutes();
45
+ this.baseUrl = await this.server.listen({
46
+ host: "127.0.0.1",
47
+ port: this.options.config.auth.portal?.port ?? 19876
48
+ });
49
+ return this.baseUrl;
50
+ }
51
+ async stop() {
52
+ this.pendingAuth.clear();
53
+ await this.server.close().catch(() => void 0);
54
+ }
55
+ async waitForAuth() {
56
+ if (this.setupComplete) {
57
+ return;
58
+ }
59
+ await this.completionPromise;
60
+ }
61
+ getUrl() {
62
+ return this.baseUrl;
63
+ }
64
+ getReauthUrl() {
65
+ return `${this.baseUrl}/auth/reauth`;
66
+ }
67
+ registerRoutes() {
68
+ this.server.get("/", async (_request, reply) => {
69
+ reply.type("text/html").send(this.renderPage());
70
+ });
71
+ this.server.get("/auth/reauth", async (_request, reply) => {
72
+ reply.type("text/html").send(renderReauthPage(getConfiguredProviders(this.options.config)));
73
+ });
74
+ this.server.get("/auth/google", async (request, reply) => this.startAuthFlow("google", reply, readFlow(request.query)));
75
+ this.server.get("/auth/apple", async (request, reply) => this.startAuthFlow("apple", reply, readFlow(request.query)));
76
+ this.server.get("/auth/callback", async (request, reply) => {
77
+ await this.handleOAuthCallback("google", request.query, reply);
78
+ });
79
+ this.server.post("/auth/apple/callback", async (request, reply) => {
80
+ await this.handleOAuthCallback("apple", request.body ?? {}, reply);
81
+ });
82
+ this.server.get("/api/status", async () => {
83
+ const auth = this.getAuthStatus();
84
+ return {
85
+ authenticated: auth.authenticated,
86
+ authMode: auth.authMode,
87
+ authState: auth.state,
88
+ address: auth.address,
89
+ auth,
90
+ did: this.options.state?.did ?? null,
91
+ setupComplete: this.setupComplete,
92
+ spendingLimitMist: getCurrentDailyLimitMist(this.options.config).toString()
93
+ };
94
+ });
95
+ this.server.post("/api/settings", async (request, reply) => {
96
+ const previousSettings = snapshotPortalSettings(this.options.config);
97
+ try {
98
+ const body = request.body ?? {};
99
+ const nextLimit = normalizeDailyLimit(body);
100
+ updateDailyLimit(this.options.config, nextLimit);
101
+ this.options.state?.spendingPolicy.updatePolicy(this.options.config.spending);
102
+ await this.options.onSettingsSaved?.(this.options.config);
103
+ await saveConfig(this.options.config, this.options.configPath);
104
+ this.options.logger?.info({ configPath: this.options.configPath }, "Portal settings persisted");
105
+ this.setupComplete = true;
106
+ this.resolveCompletion();
107
+ return {
108
+ ok: true,
109
+ address: await this.options.authProvider.getAddress(),
110
+ spendingLimitMist: nextLimit.toString()
111
+ };
112
+ } catch (error) {
113
+ restorePortalSettings(this.options.config, previousSettings);
114
+ this.options.state?.spendingPolicy.updatePolicy(this.options.config.spending);
115
+ if (!isInputValidationError(error)) {
116
+ this.options.logger?.warn({ err: error, configPath: this.options.configPath }, "Failed to persist portal settings");
117
+ }
118
+ return reply.code(isInputValidationError(error) ? 400 : 500).send({
119
+ ok: false,
120
+ error: getSafeErrorMessage(error, "Unable to save settings.")
121
+ });
122
+ }
123
+ });
124
+ this.server.get("/network", async (_request, reply) => {
125
+ reply.type("text/html").send(renderNetworkPage(this.options.config.network));
126
+ });
127
+ this.server.get("/api/network", async () => {
128
+ return { ...this.options.config.network };
129
+ });
130
+ this.server.post("/api/network", async (request, reply) => {
131
+ const previousNetwork = { ...this.options.config.network };
132
+ try {
133
+ const body = request.body ?? {};
134
+ const validated = validateNetworkInput(body);
135
+ this.options.config.network = { ...this.options.config.network, ...validated };
136
+ await saveConfig(this.options.config, this.options.configPath);
137
+ await this.options.onSettingsSaved?.(this.options.config);
138
+ this.options.logger?.info({ configPath: this.options.configPath }, "Portal network config persisted");
139
+ return { ok: true, network: { ...this.options.config.network } };
140
+ } catch (error) {
141
+ this.options.config.network = previousNetwork;
142
+ const isValidation = isNetworkValidationError(error);
143
+ if (!isValidation) {
144
+ this.options.logger?.warn({ err: error, configPath: this.options.configPath }, "Failed to persist network config");
145
+ }
146
+ return reply.code(isValidation ? 400 : 500).send({
147
+ ok: false,
148
+ error: getSafeErrorMessage(error, "Unable to save network settings.")
149
+ });
150
+ }
151
+ });
152
+ this.server.get("/wallet", async (_request, reply) => {
153
+ reply.type("text/html").send(renderWalletPage());
154
+ });
155
+ this.server.get("/api/wallet", async () => {
156
+ const state = this.options.state;
157
+ if (!state) {
158
+ return { error: "Daemon state not available." };
159
+ }
160
+ const balanceMist = await state.suiClient.getBalance(state.address);
161
+ return {
162
+ did: state.did,
163
+ address: state.address,
164
+ balanceMist: balanceMist.toString(),
165
+ balanceSui: formatMistToSui(balanceMist),
166
+ spendingToday: formatMistToSui(state.spendingPolicy.getSpent("day")),
167
+ spendingThisHour: formatMistToSui(state.spendingPolicy.getSpent("hour")),
168
+ spendingThisMonth: formatMistToSui(state.spendingPolicy.getSpent("month")),
169
+ dailyLimit: formatDailyLimit(this.options.config)
170
+ };
171
+ });
172
+ this.server.get("/discover", async (_request, reply) => {
173
+ reply.type("text/html").send(renderDiscoverPage());
174
+ });
175
+ this.server.get("/api/discover", async (request, reply) => {
176
+ const state = this.options.state;
177
+ if (!state) {
178
+ return { error: "Daemon state not available." };
179
+ }
180
+ const query = request.query ?? {};
181
+ const capability = (query.capability ?? "").trim();
182
+ if (!capability) {
183
+ return { capability: "", agents: [] };
184
+ }
185
+ if (capability.length > 200) {
186
+ reply.code(400);
187
+ return { error: "Capability query must be 200 characters or fewer." };
188
+ }
189
+ const limit = Math.min(Math.max(Number(query.limit) || 10, 1), 50);
190
+ try {
191
+ const agents = await state.registryClient.discoverByCapability(capability, limit, {});
192
+ return {
193
+ capability,
194
+ agents: agents.map((agent) => ({
195
+ name: agent.name,
196
+ did: agent.did,
197
+ active: agent.active,
198
+ capabilities: agent.capabilities.map((cap) => ({
199
+ name: cap.name,
200
+ priceMist: cap.pricing.amount.toString(),
201
+ rail: cap.pricing.rail
202
+ })),
203
+ endpoint: agent.endpoint
204
+ }))
205
+ };
206
+ } catch (err) {
207
+ reply.code(502);
208
+ return { error: "Failed to query the registry. The network may be unavailable.", capability };
209
+ }
210
+ });
211
+ this.server.get("/tasks", async (_request, reply) => {
212
+ reply.type("text/html").send(renderTasksPage());
213
+ });
214
+ this.server.get("/api/tasks", async () => {
215
+ const state = this.options.state;
216
+ if (!state) {
217
+ return { error: "Daemon state not available." };
218
+ }
219
+ const recentEntries = state.spendingPolicy.getRecentEntries(50).map((e) => ({
220
+ id: e.id,
221
+ amountSui: formatMistToSui(e.amountBaseUnits),
222
+ rail: e.rail,
223
+ taskId: e.taskId ?? null,
224
+ appId: e.appId ?? null,
225
+ timestamp: e.timestamp
226
+ }));
227
+ return {
228
+ spending: {
229
+ hour: formatMistToSui(state.spendingPolicy.getSpent("hour")),
230
+ day: formatMistToSui(state.spendingPolicy.getSpent("day")),
231
+ month: formatMistToSui(state.spendingPolicy.getSpent("month"))
232
+ },
233
+ dailyLimit: formatDailyLimit(this.options.config),
234
+ providerRunning: state.getStatusBase().providerRunning,
235
+ recentEntries
236
+ };
237
+ });
238
+ }
239
+ async startAuthFlow(provider, reply, flow = "setup") {
240
+ if (!isProviderConfigured(this.options.config, provider)) {
241
+ return reply.code(404).type("text/html").send(renderMessagePage("Authentication unavailable", `${capitalize(provider)} sign-in is not configured.`));
242
+ }
243
+ try {
244
+ this.pruneExpiredPendingAuth();
245
+ const state = randomBytes(16).toString("hex");
246
+ const { verifier, challenge } = createPkcePair();
247
+ this.setActiveOAuthConfig(provider);
248
+ const authRequest = await this.options.authProvider.createAuthorizationRequest({
249
+ redirectUri: this.getRedirectUri(provider),
250
+ state,
251
+ codeChallenge: challenge,
252
+ scopes: provider === "apple" ? ["name", "email"] : void 0
253
+ });
254
+ this.pendingAuth.set(state, {
255
+ provider,
256
+ flow,
257
+ codeVerifier: verifier,
258
+ pendingSession: authRequest.pendingSession,
259
+ createdAt: Date.now()
260
+ });
261
+ return reply.redirect(authRequest.authorizationUrl);
262
+ } catch (error) {
263
+ return reply.code(500).type("text/html").send(renderMessagePage("Authentication failed", getSafeErrorMessage(error, "Unable to start sign-in.")));
264
+ }
265
+ }
266
+ async handleOAuthCallback(provider, payload, reply) {
267
+ const code = readOptionalString(payload.code);
268
+ const error = readOptionalString(payload.error);
269
+ const errorDescription = readOptionalString(payload.error_description);
270
+ const idToken = readOptionalString(payload.id_token);
271
+ const state = readOptionalString(payload.state);
272
+ this.pruneExpiredPendingAuth();
273
+ if (error) {
274
+ if (state) {
275
+ this.pendingAuth.delete(state);
276
+ }
277
+ reply.code(400).type("text/html").send(renderMessagePage("Authentication failed", getOAuthErrorDetail(error, errorDescription)));
278
+ return;
279
+ }
280
+ if (!state) {
281
+ reply.code(400).type("text/html").send(renderMessagePage("Authentication failed", "Missing callback state."));
282
+ return;
283
+ }
284
+ const pending = this.pendingAuth.get(state);
285
+ if (!pending || pending.provider !== provider) {
286
+ reply.code(400).type("text/html").send(renderMessagePage("Authentication failed", "Unknown login state."));
287
+ return;
288
+ }
289
+ if (Date.now() - pending.createdAt > PENDING_AUTH_TTL_MS) {
290
+ this.pendingAuth.delete(state);
291
+ reply.code(400).type("text/html").send(renderMessagePage("Authentication failed", "Login session expired. Please try again."));
292
+ return;
293
+ }
294
+ this.pendingAuth.delete(state);
295
+ try {
296
+ this.setActiveOAuthConfig(provider);
297
+ const session = provider === "apple" ? await this.authenticateAppleCallback(idToken, pending) : await this.authenticateGoogleCallback(code, pending);
298
+ await this.options.onAuthenticated?.(session);
299
+ reply.type("text/html").send(pending.flow === "reauth" ? renderReauthCompletePage() : this.renderPage());
300
+ } catch (callbackError) {
301
+ reply.code(502).type("text/html").send(renderMessagePage("Authentication failed", getSafeErrorMessage(callbackError, "Unable to complete sign-in.")));
302
+ }
303
+ }
304
+ async authenticateGoogleCallback(code, pending) {
305
+ if (!code) {
306
+ throw new Error("Missing authorization code.");
307
+ }
308
+ const tokens = await this.options.authProvider.exchangeAuthorizationCode(
309
+ code,
310
+ pending.codeVerifier,
311
+ this.getRedirectUri("google")
312
+ );
313
+ return this.options.authProvider.authenticateWithJwt(tokens.jwt, {
314
+ pendingSession: pending.pendingSession,
315
+ refreshToken: tokens.refreshToken
316
+ });
317
+ }
318
+ async authenticateAppleCallback(idToken, pending) {
319
+ if (!idToken) {
320
+ throw new Error("Missing Apple identity token.");
321
+ }
322
+ return this.options.authProvider.authenticateWithJwt(idToken, {
323
+ pendingSession: pending.pendingSession
324
+ });
325
+ }
326
+ pruneExpiredPendingAuth() {
327
+ const expiresBefore = Date.now() - PENDING_AUTH_TTL_MS;
328
+ for (const [state, pending] of this.pendingAuth.entries()) {
329
+ if (pending.createdAt <= expiresBefore) {
330
+ this.pendingAuth.delete(state);
331
+ }
332
+ }
333
+ }
334
+ setActiveOAuthConfig(provider) {
335
+ this.options.authProvider.setOAuthConfig({
336
+ ...this.options.authProvider.getOAuthConfig(),
337
+ ...buildOAuthConfig(this.options.config, this.getRedirectUri(provider), provider)
338
+ });
339
+ }
340
+ getRedirectUri(provider) {
341
+ return provider === "apple" ? `${this.baseUrl}/auth/apple/callback` : `${this.baseUrl}/auth/callback`;
342
+ }
343
+ getAuthStatus() {
344
+ const fallbackSession = this.options.authProvider.getSession();
345
+ const expiresAt = getJwtExpiryMs(fallbackSession?.jwt);
346
+ return this.options.getAuthStatus?.() ?? {
347
+ authMode: "zklogin",
348
+ authenticated: this.options.authProvider.isAuthenticated(),
349
+ state: this.options.authProvider.isAuthenticated() ? "authenticated" : "reauth_required",
350
+ address: fallbackSession?.address ?? null,
351
+ expiresAt,
352
+ expiresInMs: expiresAt === null ? null : expiresAt - Date.now(),
353
+ refreshAvailable: Boolean(fallbackSession?.refreshToken),
354
+ lastError: null,
355
+ updatedAt: Date.now()
356
+ };
357
+ }
358
+ renderPage() {
359
+ const authStatus = this.getAuthStatus();
360
+ if (!this.options.authProvider.isAuthenticated()) {
361
+ return this.options.getAuthStatus && (authStatus.state === "expired" || authStatus.state === "reauth_required") ? renderReauthPage(getConfiguredProviders(this.options.config)) : renderWelcomePage(getConfiguredProviders(this.options.config));
362
+ }
363
+ return renderSetupPage({
364
+ address: this.options.authProvider.getSession()?.address ?? "",
365
+ dailyLimitMist: getCurrentDailyLimitMist(this.options.config),
366
+ setupComplete: this.setupComplete
367
+ });
368
+ }
369
+ };
370
+ function renderWelcomePage(providers) {
371
+ const detail = providers.length === 0 ? "Configure Google or Apple sign-in to create a Sui wallet without managing private keys." : providers.length === 1 ? `Sign in with ${capitalize(providers[0])} to create a Sui wallet without managing private keys.` : "Sign in with Google or Apple to create a Sui wallet without managing private keys.";
372
+ return renderAuthPage({
373
+ title: "Welcome to Agentic Mesh",
374
+ detail,
375
+ providers,
376
+ flow: "setup"
377
+ });
378
+ }
379
+ function renderReauthPage(providers) {
380
+ return renderAuthPage({
381
+ title: "Your session has expired",
382
+ detail: "Re-authenticate to resume wallet-backed operations in Agentic Mesh.",
383
+ providers,
384
+ flow: "reauth"
385
+ });
386
+ }
387
+ function renderReauthCompletePage() {
388
+ return `<!doctype html>
389
+ <html lang="en">
390
+ <head>
391
+ <meta charset="utf-8" />
392
+ <title>Authentication restored</title>
393
+ <style>${BASE_STYLES}</style>
394
+ </head>
395
+ <body>
396
+ <main class="card">
397
+ <h1>Authentication restored</h1>
398
+ <p>Your session is active again. This window will close automatically.</p>
399
+ </main>
400
+ <script>
401
+ setTimeout(() => {
402
+ window.close();
403
+ setTimeout(() => {
404
+ window.location.replace('/');
405
+ }, 250);
406
+ }, 150);
407
+ </script>
408
+ </body>
409
+ </html>`;
410
+ }
411
+ function renderAuthPage(params) {
412
+ const buttons = params.providers.map((provider) => renderAuthButton(provider, params.flow)).join("");
413
+ return `<!doctype html>
414
+ <html lang="en">
415
+ <head>
416
+ <meta charset="utf-8" />
417
+ <title>Agentic Mesh Setup</title>
418
+ <style>${BASE_STYLES}</style>
419
+ </head>
420
+ <body>
421
+ <main class="card">
422
+ <h1>${escapeHtml(params.title)}</h1>
423
+ <p>${escapeHtml(params.detail)}</p>
424
+ ${buttons ? `<div class="auth-buttons">${buttons}</div>` : '<p class="error">No OAuth providers are configured.</p>'}
425
+ </main>
426
+ </body>
427
+ </html>`;
428
+ }
429
+ function renderAuthButton(provider, flow) {
430
+ const href = `/auth/${provider}?flow=${flow}`;
431
+ if (provider === "apple") {
432
+ return `<a class="button button--apple" href="${href}"><span class="button__icon" aria-hidden="true">\uF8FF</span><span>Sign in with Apple</span></a>`;
433
+ }
434
+ return `<a class="button button--google" href="${href}"><span>Sign in with Google</span></a>`;
435
+ }
436
+ function renderSetupPage(params) {
437
+ const currentLimitSui = formatMistToSui(params.dailyLimitMist);
438
+ const successMessage = params.setupComplete ? '<p class="success">Setup complete. You can return to your app.</p>' : "";
439
+ const title = params.setupComplete ? "Agentic Mesh is ready" : "Finish setup";
440
+ return `<!doctype html>
441
+ <html lang="en">
442
+ <head>
443
+ <meta charset="utf-8" />
444
+ <title>Agentic Mesh Setup</title>
445
+ <style>${BASE_STYLES}${INNER_PAGE_STYLES}</style>
446
+ </head>
447
+ <body>
448
+ <main class="card">
449
+ ${PORTAL_NAV}
450
+ <h1>${escapeHtml(title)}</h1>
451
+ <p>Your Sui address:</p>
452
+ <code>${escapeHtml(params.address)}</code>
453
+ <label for="limit">Daily spending limit (SUI)</label>
454
+ <input id="limit" type="range" min="1" max="100" step="1" value="${escapeHtml(currentLimitSui)}" />
455
+ <div id="limit-value">${escapeHtml(currentLimitSui)} SUI</div>
456
+ <button class="button" id="finish">Finish Setup</button>
457
+ <p class="error" id="status" hidden></p>
458
+ ${successMessage}
459
+ </main>
460
+ <script>
461
+ const slider = document.getElementById('limit');
462
+ const output = document.getElementById('limit-value');
463
+ const button = document.getElementById('finish');
464
+ const status = document.getElementById('status');
465
+ slider.addEventListener('input', () => {
466
+ output.textContent = slider.value + ' SUI';
467
+ });
468
+ button.addEventListener('click', async () => {
469
+ button.disabled = true;
470
+ status.hidden = true;
471
+ try {
472
+ const response = await fetch('/api/settings', {
473
+ method: 'POST',
474
+ headers: { 'content-type': 'application/json' },
475
+ body: JSON.stringify({ dailyLimitSui: slider.value }),
476
+ });
477
+ if (!response.ok) {
478
+ const body = await response.json().catch(() => ({}));
479
+ throw new Error(typeof body.error === 'string' ? body.error : 'Unable to save settings.');
480
+ }
481
+ window.location.href = '/';
482
+ } catch (error) {
483
+ status.textContent = error instanceof Error ? error.message : 'Unable to save settings.';
484
+ status.hidden = false;
485
+ button.disabled = false;
486
+ }
487
+ });
488
+ </script>
489
+ </body>
490
+ </html>`;
491
+ }
492
+ function renderNetworkPage(network) {
493
+ return `<!doctype html>
494
+ <html lang="en">
495
+ <head>
496
+ <meta charset="utf-8" />
497
+ <title>Agentic Mesh \u2014 Network</title>
498
+ <style>${BASE_STYLES}${INNER_PAGE_STYLES}${NETWORK_PAGE_STYLES}</style>
499
+ </head>
500
+ <body>
501
+ <main class="card">
502
+ ${PORTAL_NAV}
503
+ <h1>Network Configuration</h1>
504
+ <p>Configure which Sui network the daemon connects to.</p>
505
+
506
+ <div class="presets">
507
+ <button class="preset" data-rpc="https://fullnode.devnet.sui.io:443" data-faucet="https://faucet.devnet.sui.io">Devnet</button>
508
+ <button class="preset" data-rpc="https://fullnode.testnet.sui.io:443" data-faucet="https://faucet.testnet.sui.io">Testnet</button>
509
+ <button class="preset" data-rpc="http://127.0.0.1:9000" data-faucet="http://127.0.0.1:9123">Local</button>
510
+ </div>
511
+
512
+ <label for="rpcUrl">RPC URL <span class="required">*</span></label>
513
+ <input id="rpcUrl" type="url" value="${escapeAttr(network.rpcUrl)}" placeholder="https://fullnode.devnet.sui.io:443" required />
514
+
515
+ <label for="faucetUrl">Faucet URL</label>
516
+ <input id="faucetUrl" type="url" value="${escapeAttr(network.faucetUrl)}" placeholder="https://faucet.devnet.sui.io" />
517
+
518
+ <label for="packageId">Package ID</label>
519
+ <input id="packageId" type="text" value="${escapeAttr(network.packageId)}" placeholder="0x..." />
520
+
521
+ <label for="registryId">Registry ID</label>
522
+ <input id="registryId" type="text" value="${escapeAttr(network.registryId)}" placeholder="0x..." />
523
+
524
+ <p class="hint" id="hint" hidden></p>
525
+ <button class="button" id="save">Save Network Config</button>
526
+ <p class="error" id="status" hidden></p>
527
+ <p class="success" id="success" hidden></p>
528
+ </main>
529
+ <script>
530
+ const rpcUrl = document.getElementById('rpcUrl');
531
+ const faucetUrl = document.getElementById('faucetUrl');
532
+ const packageId = document.getElementById('packageId');
533
+ const registryId = document.getElementById('registryId');
534
+ const saveBtn = document.getElementById('save');
535
+ const status = document.getElementById('status');
536
+ const successEl = document.getElementById('success');
537
+ const hint = document.getElementById('hint');
538
+
539
+ document.querySelectorAll('.preset').forEach(btn => {
540
+ btn.addEventListener('click', () => {
541
+ rpcUrl.value = btn.dataset.rpc;
542
+ faucetUrl.value = btn.dataset.faucet;
543
+ hint.textContent = 'Preset applied to RPC and Faucet URLs. Package and Registry IDs are unchanged.';
544
+ hint.hidden = false;
545
+ successEl.hidden = true;
546
+ status.hidden = true;
547
+ });
548
+ });
549
+
550
+ saveBtn.addEventListener('click', async () => {
551
+ saveBtn.disabled = true;
552
+ status.hidden = true;
553
+ successEl.hidden = true;
554
+ hint.hidden = true;
555
+ try {
556
+ const response = await fetch('/api/network', {
557
+ method: 'POST',
558
+ headers: { 'content-type': 'application/json' },
559
+ body: JSON.stringify({
560
+ rpcUrl: rpcUrl.value.trim(),
561
+ faucetUrl: faucetUrl.value.trim(),
562
+ packageId: packageId.value.trim(),
563
+ registryId: registryId.value.trim(),
564
+ }),
565
+ });
566
+ const body = await response.json().catch(() => ({}));
567
+ if (!response.ok) {
568
+ throw new Error(typeof body.error === 'string' ? body.error : 'Unable to save network settings.');
569
+ }
570
+ successEl.textContent = 'Network configuration saved. The daemon will reconnect to the configured network.';
571
+ successEl.hidden = false;
572
+ } catch (error) {
573
+ status.textContent = error instanceof Error ? error.message : 'Unable to save network settings.';
574
+ status.hidden = false;
575
+ }
576
+ saveBtn.disabled = false;
577
+ });
578
+ </script>
579
+ </body>
580
+ </html>`;
581
+ }
582
+ function renderMessagePage(title, detail) {
583
+ return `<!doctype html>
584
+ <html lang="en">
585
+ <head>
586
+ <meta charset="utf-8" />
587
+ <title>${escapeHtml(title)}</title>
588
+ <style>${BASE_STYLES}</style>
589
+ </head>
590
+ <body>
591
+ <main class="card">
592
+ <h1>${escapeHtml(title)}</h1>
593
+ <p>${escapeHtml(detail)}</p>
594
+ <a class="button" href="/">Back</a>
595
+ </main>
596
+ </body>
597
+ </html>`;
598
+ }
599
+ var PORTAL_NAV = `
600
+ <nav class="nav">
601
+ <a href="/">Settings</a> \xB7 <a href="/wallet">Wallet</a> \xB7 <a href="/discover">Discover</a> \xB7 <a href="/tasks">Tasks</a> \xB7 <a href="/network">Network</a>
602
+ </nav>`;
603
+ var INNER_PAGE_STYLES = `
604
+ .nav { margin-bottom: 16px; font-size: 0.9rem; }
605
+ .nav a { color: #60a5fa; text-decoration: none; }
606
+ .nav a:hover { text-decoration: underline; }
607
+ .stat { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #1e293b; }
608
+ .stat:last-child { border-bottom: 0; }
609
+ .stat-label { color: #94a3b8; font-size: 0.85rem; }
610
+ .stat-value { font-family: monospace; font-size: 0.9rem; }
611
+ .search-row { display: flex; gap: 8px; margin-bottom: 20px; }
612
+ .search-row input { flex: 1; padding: 10px 12px; background: #020617; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-size: 0.9rem; }
613
+ .search-row input:focus { outline: none; border-color: #2563eb; }
614
+ .search-row button { flex-shrink: 0; }
615
+ .agent-card { background: #0f172a; border: 1px solid #1e293b; border-radius: 10px; padding: 16px; margin-bottom: 12px; }
616
+ .agent-name { font-weight: 600; margin-bottom: 4px; }
617
+ .agent-did { font-family: monospace; font-size: 0.75rem; color: #64748b; word-break: break-all; }
618
+ .agent-cap { display: inline-block; background: #1e293b; border-radius: 6px; padding: 4px 10px; margin: 6px 4px 0 0; font-size: 0.8rem; }
619
+ #results-empty { color: #94a3b8; font-style: italic; }
620
+ `;
621
+ function renderWalletPage() {
622
+ return `<!doctype html>
623
+ <html lang="en">
624
+ <head>
625
+ <meta charset="utf-8" />
626
+ <title>Agentic Mesh \u2014 Wallet</title>
627
+ <style>${BASE_STYLES}${INNER_PAGE_STYLES}</style>
628
+ </head>
629
+ <body>
630
+ <main class="card">
631
+ ${PORTAL_NAV}
632
+ <h1>Wallet</h1>
633
+ <div id="loading">Loading\u2026</div>
634
+ <div id="content" hidden>
635
+ <div class="stat"><span class="stat-label">Address</span><span class="stat-value" id="address"></span></div>
636
+ <div class="stat"><span class="stat-label">DID</span><span class="stat-value" id="did"></span></div>
637
+ <div class="stat"><span class="stat-label">Balance</span><span class="stat-value" id="balance"></span></div>
638
+ <div class="stat"><span class="stat-label">Spent today</span><span class="stat-value" id="spent-day"></span></div>
639
+ <div class="stat"><span class="stat-label">Spent this hour</span><span class="stat-value" id="spent-hour"></span></div>
640
+ <div class="stat"><span class="stat-label">Spent this month</span><span class="stat-value" id="spent-month"></span></div>
641
+ <div class="stat"><span class="stat-label">Daily limit</span><span class="stat-value" id="limit"></span></div>
642
+ </div>
643
+ </main>
644
+ <script>
645
+ (async () => {
646
+ try {
647
+ const res = await fetch('/api/wallet');
648
+ const data = await res.json();
649
+ document.getElementById('address').textContent = data.address ?? '\u2014';
650
+ document.getElementById('did').textContent = data.did ?? '\u2014';
651
+ document.getElementById('balance').textContent = (data.balanceSui ?? '\u2014') + ' SUI';
652
+ document.getElementById('spent-day').textContent = (data.spendingToday ?? '0') + ' SUI';
653
+ document.getElementById('spent-hour').textContent = (data.spendingThisHour ?? '0') + ' SUI';
654
+ document.getElementById('spent-month').textContent = (data.spendingThisMonth ?? '0') + ' SUI';
655
+ document.getElementById('limit').textContent = data.dailyLimit ?? '\u2014';
656
+ document.getElementById('loading').hidden = true;
657
+ document.getElementById('content').hidden = false;
658
+ } catch (e) {
659
+ document.getElementById('loading').textContent = 'Failed to load wallet data.';
660
+ }
661
+ })();
662
+ </script>
663
+ </body>
664
+ </html>`;
665
+ }
666
+ function renderDiscoverPage() {
667
+ return `<!doctype html>
668
+ <html lang="en">
669
+ <head>
670
+ <meta charset="utf-8" />
671
+ <title>Agentic Mesh \u2014 Discover</title>
672
+ <style>${BASE_STYLES}${INNER_PAGE_STYLES}</style>
673
+ </head>
674
+ <body>
675
+ <main class="card">
676
+ ${PORTAL_NAV}
677
+ <h1>Discover Agents</h1>
678
+ <div class="search-row">
679
+ <input id="capability" type="text" placeholder="Enter capability name\u2026" />
680
+ <button class="button" id="search">Search</button>
681
+ </div>
682
+ <div id="results"></div>
683
+ </main>
684
+ <script>
685
+ const input = document.getElementById('capability');
686
+ const btn = document.getElementById('search');
687
+ const results = document.getElementById('results');
688
+
689
+ async function doSearch() {
690
+ const cap = input.value.trim();
691
+ if (!cap) return;
692
+ btn.disabled = true;
693
+ results.innerHTML = '<p style="color:#94a3b8">Searching\u2026</p>';
694
+ try {
695
+ const res = await fetch('/api/discover?capability=' + encodeURIComponent(cap));
696
+ const data = await res.json();
697
+ if (data.error) {
698
+ results.innerHTML = '<p class="error">' + esc(data.error) + '</p>';
699
+ return;
700
+ }
701
+ if (!data.agents || data.agents.length === 0) {
702
+ results.innerHTML = '<p id="results-empty">No agents found for this capability.</p>';
703
+ return;
704
+ }
705
+ results.innerHTML = data.agents.map(a => {
706
+ const caps = (a.capabilities || []).map(c =>
707
+ '<span class="agent-cap">' + esc(c.name) + ' \u2014 ' + esc(c.priceMist) + ' MIST</span>'
708
+ ).join('');
709
+ return '<div class="agent-card">' +
710
+ '<div class="agent-name">' + esc(a.name) + (a.active ? '' : ' <span style="color:#f87171">(inactive)</span>') + '</div>' +
711
+ '<div class="agent-did">' + esc(a.did) + '</div>' +
712
+ (caps ? '<div>' + caps + '</div>' : '') +
713
+ (a.endpoint ? '<div style="margin-top:6px;font-size:0.8rem;color:#64748b">' + esc(a.endpoint) + '</div>' : '') +
714
+ '</div>';
715
+ }).join('');
716
+ } catch (e) {
717
+ results.innerHTML = '<p class="error">Search failed.</p>';
718
+ } finally {
719
+ btn.disabled = false;
720
+ }
721
+ }
722
+
723
+ btn.addEventListener('click', doSearch);
724
+ input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
725
+
726
+ function esc(s) {
727
+ const d = document.createElement('div');
728
+ d.textContent = s ?? '';
729
+ return d.innerHTML;
730
+ }
731
+ </script>
732
+ </body>
733
+ </html>`;
734
+ }
735
+ function renderTasksPage() {
736
+ return `<!doctype html>
737
+ <html lang="en">
738
+ <head>
739
+ <meta charset="utf-8" />
740
+ <title>Agentic Mesh \u2014 Tasks</title>
741
+ <style>${BASE_STYLES}${INNER_PAGE_STYLES}
742
+ .tx-table { width: 100%; border-collapse: collapse; margin-top: 16px; font-size: 0.82rem; }
743
+ .tx-table th { text-align: left; color: #94a3b8; padding: 6px 8px; border-bottom: 1px solid #1e293b; }
744
+ .tx-table td { padding: 6px 8px; border-bottom: 1px solid #0f172a; font-family: monospace; }
745
+ .tx-table tr:hover td { background: #0f172a; }
746
+ </style>
747
+ </head>
748
+ <body>
749
+ <main class="card">
750
+ ${PORTAL_NAV}
751
+ <h1>Tasks &amp; Spending</h1>
752
+ <div id="loading">Loading\u2026</div>
753
+ <div id="content" hidden>
754
+ <div class="stat"><span class="stat-label">Spent this hour</span><span class="stat-value" id="hour"></span></div>
755
+ <div class="stat"><span class="stat-label">Spent today</span><span class="stat-value" id="day"></span></div>
756
+ <div class="stat"><span class="stat-label">Spent this month</span><span class="stat-value" id="month"></span></div>
757
+ <div class="stat"><span class="stat-label">Daily limit</span><span class="stat-value" id="limit"></span></div>
758
+ <div class="stat"><span class="stat-label">Provider running</span><span class="stat-value" id="provider"></span></div>
759
+ <h2 style="margin-top:24px;font-size:1rem;">Recent Transactions</h2>
760
+ <div id="entries"></div>
761
+ </div>
762
+ </main>
763
+ <script>
764
+ function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; }
765
+ (async () => {
766
+ try {
767
+ const res = await fetch('/api/tasks');
768
+ const data = await res.json();
769
+ document.getElementById('hour').textContent = (data.spending?.hour ?? '0') + ' SUI';
770
+ document.getElementById('day').textContent = (data.spending?.day ?? '0') + ' SUI';
771
+ document.getElementById('month').textContent = (data.spending?.month ?? '0') + ' SUI';
772
+ document.getElementById('limit').textContent = data.dailyLimit ?? '\u2014';
773
+ document.getElementById('provider').textContent = data.providerRunning ? 'Yes' : 'No';
774
+
775
+ const entries = data.recentEntries ?? [];
776
+ if (entries.length === 0) {
777
+ document.getElementById('entries').innerHTML = '<p style="color:#64748b;font-style:italic">No transactions yet.</p>';
778
+ } else {
779
+ const rows = entries.map(e => '<tr><td>' + esc(new Date(e.timestamp).toLocaleString()) + '</td><td>' + esc(e.amountSui) + ' SUI</td><td>' + esc(e.rail) + '</td><td>' + esc(e.taskId ?? '\u2014') + '</td><td>' + esc(e.appId ?? '\u2014') + '</td></tr>').join('');
780
+ document.getElementById('entries').innerHTML = '<table class="tx-table"><thead><tr><th>Time</th><th>Amount</th><th>Rail</th><th>Task</th><th>App</th></tr></thead><tbody>' + rows + '</tbody></table>';
781
+ }
782
+
783
+ document.getElementById('loading').hidden = true;
784
+ document.getElementById('content').hidden = false;
785
+ } catch (e) {
786
+ document.getElementById('loading').textContent = 'Failed to load task data.';
787
+ }
788
+ })();
789
+ </script>
790
+ </body>
791
+ </html>`;
792
+ }
793
+ function snapshotPortalSettings(config) {
794
+ return structuredClone({
795
+ auth: config.auth,
796
+ payment: config.payment,
797
+ spending: config.spending
798
+ });
799
+ }
800
+ function restorePortalSettings(target, snapshot) {
801
+ target.auth = snapshot.auth;
802
+ target.payment = snapshot.payment;
803
+ target.spending = snapshot.spending;
804
+ }
805
+ function normalizeDailyLimit(body) {
806
+ if (body.dailyLimitMist !== void 0) {
807
+ return parsePositiveBigInt(body.dailyLimitMist, "dailyLimitMist");
808
+ }
809
+ if (body.dailyLimitSui !== void 0) {
810
+ return parseSuiToMist(body.dailyLimitSui);
811
+ }
812
+ throw new Error("A daily spending limit is required.");
813
+ }
814
+ function updateDailyLimit(config, amountMist) {
815
+ const nextLimit = { amount: amountMist, interval: "day" };
816
+ const existing = config.spending.limits.findIndex((limit) => limit.interval === "day" && !limit.scope && !limit.rail);
817
+ if (existing >= 0) {
818
+ config.spending.limits[existing] = nextLimit;
819
+ return;
820
+ }
821
+ config.spending.limits.push(nextLimit);
822
+ }
823
+ function getCurrentDailyLimitMist(config) {
824
+ return config.spending.limits.find((limit) => limit.interval === "day" && !limit.scope && !limit.rail)?.amount ?? 0n;
825
+ }
826
+ function formatDailyLimit(config) {
827
+ const limit = getCurrentDailyLimitMist(config);
828
+ return limit === 0n ? "Unlimited" : formatMistToSui(limit) + " SUI";
829
+ }
830
+ function parsePositiveBigInt(value, field) {
831
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
832
+ return BigInt(Math.floor(value));
833
+ }
834
+ if (typeof value === "string" && /^\d+$/.test(value.trim())) {
835
+ return BigInt(value.trim());
836
+ }
837
+ throw new Error(`${field} must be a non-negative integer.`);
838
+ }
839
+ function parseSuiToMist(value) {
840
+ const text = typeof value === "number" ? value.toString() : value.trim();
841
+ if (!/^\d+(?:\.\d{1,9})?$/.test(text)) {
842
+ throw new Error("dailyLimitSui must be a valid SUI amount.");
843
+ }
844
+ const [whole, fraction = ""] = text.split(".");
845
+ return BigInt(whole) * 1000000000n + BigInt(fraction.padEnd(9, "0"));
846
+ }
847
+ function formatMistToSui(value) {
848
+ const whole = value / 1000000000n;
849
+ const fraction = value % 1000000000n;
850
+ if (fraction === 0n) {
851
+ return whole.toString();
852
+ }
853
+ return `${whole.toString()}.${fraction.toString().padStart(9, "0").replace(/0+$/, "")}`;
854
+ }
855
+ function escapeHtml(value) {
856
+ return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character] ?? character);
857
+ }
858
+ function getSafeErrorMessage(error, fallback) {
859
+ if (!(error instanceof Error) || !error.message.trim()) {
860
+ return fallback;
861
+ }
862
+ return error.message;
863
+ }
864
+ function getOAuthErrorDetail(error, errorDescription) {
865
+ return errorDescription?.trim() || error.replace(/_/g, " ");
866
+ }
867
+ function readFlow(value) {
868
+ if (value && typeof value === "object" && !Array.isArray(value)) {
869
+ const flow = value.flow;
870
+ if (flow === "reauth") {
871
+ return "reauth";
872
+ }
873
+ }
874
+ return "setup";
875
+ }
876
+ function getJwtExpiryMs(jwt) {
877
+ if (!jwt) {
878
+ return null;
879
+ }
880
+ const [, payload] = jwt.split(".");
881
+ if (!payload) {
882
+ return null;
883
+ }
884
+ try {
885
+ const parsed = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
886
+ const exp = parsed.exp;
887
+ const seconds = typeof exp === "number" ? exp : typeof exp === "string" && /^\d+$/.test(exp) ? Number(exp) : null;
888
+ return seconds === null ? null : seconds * 1e3;
889
+ } catch {
890
+ return null;
891
+ }
892
+ }
893
+ function readOptionalString(value) {
894
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
895
+ }
896
+ function getConfiguredProviders(config) {
897
+ return ["google", "apple"].filter((provider) => isProviderConfigured(config, provider));
898
+ }
899
+ function isProviderConfigured(config, provider) {
900
+ return provider === "google" ? Boolean(config.auth.google?.clientId) : Boolean(config.auth.apple?.clientId);
901
+ }
902
+ function capitalize(value) {
903
+ return value.charAt(0).toUpperCase() + value.slice(1);
904
+ }
905
+ function isInputValidationError(error) {
906
+ return error instanceof Error && /(dailyLimit|required|valid SUI amount|non-negative integer)/i.test(error.message);
907
+ }
908
+ function isLoopbackOrigin(origin) {
909
+ try {
910
+ const parsed = new URL(origin);
911
+ return parsed.protocol === "http:" && (parsed.hostname === "127.0.0.1" || parsed.hostname === "localhost");
912
+ } catch {
913
+ return false;
914
+ }
915
+ }
916
+ var HTML_ESCAPES = {
917
+ "&": "&amp;",
918
+ "<": "&lt;",
919
+ ">": "&gt;",
920
+ '"': "&quot;",
921
+ "'": "&#39;"
922
+ };
923
+ var BASE_STYLES = `
924
+ :root { color-scheme: dark; }
925
+ body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; display: grid; place-items: center; min-height: 100vh; margin: 0; }
926
+ .card { width: min(480px, 92vw); background: #111827; border: 1px solid #334155; border-radius: 16px; padding: 32px; box-shadow: 0 24px 48px rgba(15, 23, 42, 0.35); }
927
+ h1 { margin-top: 0; }
928
+ p, label { color: #cbd5e1; }
929
+ code { display: block; padding: 12px; border-radius: 10px; background: #020617; overflow-wrap: anywhere; margin-bottom: 20px; }
930
+ .button, button { display: inline-flex; justify-content: center; align-items: center; gap: 10px; border: 0; border-radius: 999px; background: #2563eb; color: white; padding: 12px 18px; text-decoration: none; font-weight: 600; cursor: pointer; }
931
+ .auth-buttons { display: grid; gap: 12px; margin-top: 20px; }
932
+ .button--google { background: #2563eb; }
933
+ .button--apple { background: #000; color: #fff; border: 1px solid #1f2937; }
934
+ .button__icon { font-size: 1.1rem; line-height: 1; }
935
+ input[type='range'] { width: 100%; margin: 16px 0 12px; }
936
+ .success { color: #86efac; }
937
+ .error { color: #fca5a5; }
938
+ `;
939
+ var NETWORK_PAGE_STYLES = `
940
+ .nav { margin-bottom: 16px; }
941
+ .nav a { color: #60a5fa; text-decoration: none; font-size: 0.9rem; }
942
+ .nav a:hover { text-decoration: underline; }
943
+ .presets { display: flex; gap: 8px; margin-bottom: 20px; }
944
+ .preset { background: #1e293b; border: 1px solid #475569; border-radius: 8px; color: #e2e8f0; padding: 8px 14px; font-size: 0.85rem; cursor: pointer; }
945
+ .preset:hover { background: #334155; }
946
+ label { display: block; margin-top: 16px; font-size: 0.85rem; font-weight: 600; }
947
+ .required { color: #f87171; }
948
+ input[type='url'], input[type='text'] { display: block; width: 100%; box-sizing: border-box; margin-top: 6px; padding: 10px 12px; background: #020617; border: 1px solid #334155; border-radius: 8px; color: #e2e8f0; font-family: monospace; font-size: 0.85rem; }
949
+ input[type='url']:focus, input[type='text']:focus { outline: none; border-color: #2563eb; }
950
+ .hint { color: #94a3b8; font-size: 0.85rem; margin-top: 12px; }
951
+ #save { margin-top: 24px; width: 100%; }
952
+ `;
953
+ function escapeAttr(value) {
954
+ return value.replace(/[&"'<>]/g, (character) => HTML_ESCAPES[character] ?? character);
955
+ }
956
+ function validateNetworkInput(body) {
957
+ const rpcUrl = (body.rpcUrl ?? "").trim();
958
+ const faucetUrl = (body.faucetUrl ?? "").trim();
959
+ const packageId = (body.packageId ?? "").trim();
960
+ const registryId = (body.registryId ?? "").trim();
961
+ if (!rpcUrl) {
962
+ throw new NetworkValidationError("RPC URL is required.");
963
+ }
964
+ validateHttpUrl(rpcUrl, "RPC URL");
965
+ if (faucetUrl) {
966
+ validateHttpUrl(faucetUrl, "Faucet URL");
967
+ }
968
+ if (packageId && !isValidHexId(packageId)) {
969
+ throw new NetworkValidationError("Package ID must be a valid hex address starting with 0x.");
970
+ }
971
+ if (registryId && !isValidHexId(registryId)) {
972
+ throw new NetworkValidationError("Registry ID must be a valid hex address starting with 0x.");
973
+ }
974
+ return { rpcUrl, faucetUrl, packageId, registryId };
975
+ }
976
+ function validateHttpUrl(value, label) {
977
+ try {
978
+ const parsed = new URL(value);
979
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
980
+ throw new NetworkValidationError(`${label} must use http or https protocol.`);
981
+ }
982
+ } catch (error) {
983
+ if (error instanceof NetworkValidationError) {
984
+ throw error;
985
+ }
986
+ throw new NetworkValidationError(`${label} is not a valid URL.`);
987
+ }
988
+ }
989
+ function isValidHexId(value) {
990
+ return /^0x[0-9a-fA-F]{1,64}$/.test(value);
991
+ }
992
+ var NetworkValidationError = class extends Error {
993
+ constructor(message) {
994
+ super(message);
995
+ this.name = "NetworkValidationError";
996
+ }
997
+ };
998
+ function isNetworkValidationError(error) {
999
+ return error instanceof NetworkValidationError;
1000
+ }
1001
+
1002
+ export {
1003
+ PortalServer
1004
+ };
1005
+ //# sourceMappingURL=chunk-VHECO532.js.map