atproto user agency toolkit for individuals and groups
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add admin monitoring API endpoints for dashboard visibility

Four new authenticated endpoints expose existing internal state:
- getOverview: aggregated system status (network, replication, firehose, policy, verification)
- getDidStatus: per-DID detail (sync state, block/blob counts, peer endpoints, effective policy)
- getNetworkStatus: P2P connectivity (peerId, multiaddrs, connections)
- getPolicies: policy engine configuration and explicit DID lists

+696
+17
src/index.ts
··· 11 11 import * as sync from "./xrpc/sync.js"; 12 12 import * as repo from "./xrpc/repo.js"; 13 13 import * as server from "./xrpc/server.js"; 14 + import * as admin from "./xrpc/admin.js"; 14 15 import { respondToChallenge } from "./replication/challenge-response/challenge-responder.js"; 15 16 import { serializeResponse } from "./replication/challenge-response/http-transport.js"; 16 17 import type { StorageChallenge } from "./replication/challenge-response/types.js"; ··· 546 547 await offerManager.revokeOffer(body.subject); 547 548 return c.json({ message: "Offer revoked" }); 548 549 }); 550 + 551 + // ============================================ 552 + // Admin monitoring 553 + // ============================================ 554 + app.get("/xrpc/org.p2pds.admin.getOverview", requireAuth, (c) => 555 + admin.getOverview(c, networkService, replicationManager), 556 + ); 557 + app.get("/xrpc/org.p2pds.admin.getDidStatus", requireAuth, (c) => 558 + admin.getDidStatus(c, replicationManager), 559 + ); 560 + app.get("/xrpc/org.p2pds.admin.getNetworkStatus", requireAuth, (c) => 561 + admin.getNetworkStatus(c, networkService), 562 + ); 563 + app.get("/xrpc/org.p2pds.admin.getPolicies", requireAuth, (c) => 564 + admin.getPolicies(c, replicationManager), 565 + ); 549 566 550 567 // ============================================ 551 568 // MST Proof serving
+536
src/xrpc/admin.test.ts
··· 1 + import { describe, it, expect, beforeEach, afterEach } from "vitest"; 2 + import { mkdtempSync, rmSync } from "node:fs"; 3 + import { tmpdir } from "node:os"; 4 + import { join } from "node:path"; 5 + import Database from "better-sqlite3"; 6 + import { IpfsService } from "../ipfs.js"; 7 + import { RepoManager } from "../repo-manager.js"; 8 + import { ReplicationManager } from "../replication/replication-manager.js"; 9 + import { Firehose } from "../firehose.js"; 10 + import { createApp } from "../index.js"; 11 + import { PolicyEngine } from "../policy/engine.js"; 12 + import type { Config } from "../config.js"; 13 + import type { NetworkService } from "../ipfs.js"; 14 + import type { PolicySet } from "../policy/types.js"; 15 + 16 + function testConfig(dataDir: string, replicateDids: string[] = []): Config { 17 + return { 18 + DID: "did:plc:test123", 19 + HANDLE: "test.example.com", 20 + PDS_HOSTNAME: "test.example.com", 21 + AUTH_TOKEN: "test-auth-token", 22 + SIGNING_KEY: 23 + "0000000000000000000000000000000000000000000000000000000000000001", 24 + SIGNING_KEY_PUBLIC: "zQ3shP2mWsZYWgvZM9GJ3EvMfRXQJwuTh6BdXLvJB9gFhT3Lr", 25 + JWT_SECRET: "test-jwt-secret", 26 + PASSWORD_HASH: "$2a$10$test", 27 + DATA_DIR: dataDir, 28 + PORT: 3000, 29 + IPFS_ENABLED: true, 30 + IPFS_NETWORKING: false, 31 + REPLICATE_DIDS: replicateDids, 32 + FIREHOSE_URL: "wss://localhost/xrpc/com.atproto.sync.subscribeRepos", 33 + FIREHOSE_ENABLED: false, 34 + }; 35 + } 36 + 37 + const AUTH_HEADERS = { 38 + Authorization: "Bearer test-auth-token", 39 + }; 40 + 41 + function authGet(app: ReturnType<typeof createApp>, path: string) { 42 + return app.request(path, { headers: AUTH_HEADERS }, {}); 43 + } 44 + 45 + function noAuthGet(app: ReturnType<typeof createApp>, path: string) { 46 + return app.request(path, undefined, {}); 47 + } 48 + 49 + // ============================================ 50 + // Auth check (covers all admin endpoints) 51 + // ============================================ 52 + 53 + describe("Admin endpoints: auth required", () => { 54 + let tmpDir: string; 55 + let db: InstanceType<typeof Database>; 56 + let app: ReturnType<typeof createApp>; 57 + 58 + beforeEach(() => { 59 + tmpDir = mkdtempSync(join(tmpdir(), "admin-auth-test-")); 60 + db = new Database(join(tmpDir, "test.db")); 61 + const config = testConfig(tmpDir); 62 + const repoManager = new RepoManager(db, config); 63 + repoManager.init(); 64 + const firehose = new Firehose(repoManager); 65 + app = createApp(config, repoManager, firehose); 66 + }); 67 + 68 + afterEach(() => { 69 + db.close(); 70 + try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} 71 + }); 72 + 73 + it("returns 401 for all admin endpoints without auth", async () => { 74 + const endpoints = [ 75 + "/xrpc/org.p2pds.admin.getOverview", 76 + "/xrpc/org.p2pds.admin.getDidStatus?did=did:plc:test", 77 + "/xrpc/org.p2pds.admin.getNetworkStatus", 78 + "/xrpc/org.p2pds.admin.getPolicies", 79 + ]; 80 + 81 + for (const endpoint of endpoints) { 82 + const res = await noAuthGet(app, endpoint); 83 + expect(res.status, `${endpoint} should require auth`).toBe(401); 84 + } 85 + }); 86 + }); 87 + 88 + // ============================================ 89 + // getOverview 90 + // ============================================ 91 + 92 + describe("Admin: getOverview", () => { 93 + let tmpDir: string; 94 + let db: InstanceType<typeof Database>; 95 + 96 + beforeEach(() => { 97 + tmpDir = mkdtempSync(join(tmpdir(), "admin-overview-test-")); 98 + db = new Database(join(tmpDir, "test.db")); 99 + }); 100 + 101 + afterEach(() => { 102 + db.close(); 103 + try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} 104 + }); 105 + 106 + it("returns overview without replication", async () => { 107 + const config = testConfig(tmpDir); 108 + const repoManager = new RepoManager(db, config); 109 + repoManager.init(); 110 + const firehose = new Firehose(repoManager); 111 + const app = createApp(config, repoManager, firehose); 112 + 113 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getOverview"); 114 + expect(res.status).toBe(200); 115 + 116 + const json = await res.json() as Record<string, unknown>; 117 + expect(json.version).toBe("0.1.0"); 118 + expect(json.did).toBe("did:plc:test123"); 119 + expect(json.network).toBeNull(); 120 + expect(json.replication).toEqual({ 121 + enabled: false, 122 + trackedDids: [], 123 + syncStates: [], 124 + }); 125 + expect(json.firehose).toBeNull(); 126 + expect(json.policy).toBeNull(); 127 + }); 128 + 129 + it("returns overview with replication enabled", async () => { 130 + const trackedDid = "did:plc:tracked1"; 131 + const config = testConfig(tmpDir, [trackedDid]); 132 + const repoManager = new RepoManager(db, config); 133 + repoManager.init(); 134 + 135 + const mockNetworkService: NetworkService = { 136 + provideBlocks: async () => {}, 137 + publishCommitNotification: async () => {}, 138 + onCommitNotification: () => {}, 139 + subscribeCommitTopics: () => {}, 140 + unsubscribeCommitTopics: () => {}, 141 + getPeerId: () => "12D3KooWTest", 142 + getMultiaddrs: () => ["/ip4/127.0.0.1/tcp/4001"], 143 + getConnectionCount: () => 3, 144 + getRemoteAddrs: () => [], 145 + publishIdentityNotification: async () => {}, 146 + onIdentityNotification: () => {}, 147 + subscribeIdentityTopics: () => {}, 148 + unsubscribeIdentityTopics: () => {}, 149 + }; 150 + 151 + const mockDidResolver = { 152 + resolve: async () => null, 153 + }; 154 + 155 + const replicationManager = new ReplicationManager( 156 + db, 157 + config, 158 + repoManager, 159 + { putBlock: async () => {}, getBlock: async () => null, hasBlock: async () => false, putBlocks: async () => {} }, 160 + mockNetworkService, 161 + mockDidResolver as any, 162 + ); 163 + // Initialize schema without full init (avoids network calls) 164 + replicationManager.getSyncStorage().initSchema(); 165 + 166 + const firehose = new Firehose(repoManager); 167 + const app = createApp( 168 + config, 169 + repoManager, 170 + firehose, 171 + undefined, 172 + mockNetworkService, 173 + undefined, 174 + replicationManager, 175 + ); 176 + 177 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getOverview"); 178 + expect(res.status).toBe(200); 179 + 180 + const json = await res.json() as Record<string, unknown>; 181 + expect(json.version).toBe("0.1.0"); 182 + expect(json.network).toEqual({ 183 + peerId: "12D3KooWTest", 184 + multiaddrs: ["/ip4/127.0.0.1/tcp/4001"], 185 + connections: 3, 186 + }); 187 + const repl = json.replication as Record<string, unknown>; 188 + expect(repl.enabled).toBe(true); 189 + expect(json.firehose).toBeNull(); 190 + expect(json.policy).toBeNull(); 191 + }); 192 + }); 193 + 194 + // ============================================ 195 + // getDidStatus 196 + // ============================================ 197 + 198 + describe("Admin: getDidStatus", () => { 199 + let tmpDir: string; 200 + let db: InstanceType<typeof Database>; 201 + let config: Config; 202 + let repoManager: RepoManager; 203 + let replicationManager: ReplicationManager; 204 + let app: ReturnType<typeof createApp>; 205 + 206 + const trackedDid = "did:plc:tracked1"; 207 + 208 + beforeEach(() => { 209 + tmpDir = mkdtempSync(join(tmpdir(), "admin-didstatus-test-")); 210 + db = new Database(join(tmpDir, "test.db")); 211 + config = testConfig(tmpDir, [trackedDid]); 212 + repoManager = new RepoManager(db, config); 213 + repoManager.init(); 214 + 215 + const mockNetworkService: NetworkService = { 216 + provideBlocks: async () => {}, 217 + publishCommitNotification: async () => {}, 218 + onCommitNotification: () => {}, 219 + subscribeCommitTopics: () => {}, 220 + unsubscribeCommitTopics: () => {}, 221 + getPeerId: () => null, 222 + getMultiaddrs: () => [], 223 + getConnectionCount: () => 0, 224 + getRemoteAddrs: () => [], 225 + publishIdentityNotification: async () => {}, 226 + onIdentityNotification: () => {}, 227 + subscribeIdentityTopics: () => {}, 228 + unsubscribeIdentityTopics: () => {}, 229 + }; 230 + 231 + replicationManager = new ReplicationManager( 232 + db, 233 + config, 234 + repoManager, 235 + { putBlock: async () => {}, getBlock: async () => null, hasBlock: async () => false, putBlocks: async () => {} }, 236 + mockNetworkService, 237 + { resolve: async () => null } as any, 238 + ); 239 + replicationManager.getSyncStorage().initSchema(); 240 + 241 + const firehose = new Firehose(repoManager); 242 + app = createApp( 243 + config, 244 + repoManager, 245 + firehose, 246 + undefined, 247 + mockNetworkService, 248 + undefined, 249 + replicationManager, 250 + ); 251 + }); 252 + 253 + afterEach(() => { 254 + db.close(); 255 + try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} 256 + }); 257 + 258 + it("returns 400 when did param is missing", async () => { 259 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getDidStatus"); 260 + expect(res.status).toBe(400); 261 + const json = await res.json() as Record<string, unknown>; 262 + expect(json.error).toBe("MissingParameter"); 263 + }); 264 + 265 + it("returns data for a tracked DID", async () => { 266 + const syncStorage = replicationManager.getSyncStorage(); 267 + syncStorage.upsertState({ 268 + did: trackedDid, 269 + pdsEndpoint: "https://pds.example.com", 270 + }); 271 + syncStorage.updateSyncProgress(trackedDid, "rev1", "bafyroot1"); 272 + 273 + const res = await authGet( 274 + app, 275 + `/xrpc/org.p2pds.admin.getDidStatus?did=${trackedDid}`, 276 + ); 277 + expect(res.status).toBe(200); 278 + 279 + const json = await res.json() as Record<string, unknown>; 280 + expect(json.did).toBe(trackedDid); 281 + expect(json.syncState).not.toBeNull(); 282 + const syncState = json.syncState as Record<string, unknown>; 283 + expect(syncState.lastSyncRev).toBe("rev1"); 284 + expect(json.blockCount).toBe(0); 285 + expect(json.blobCount).toBe(0); 286 + expect(json.peerEndpoints).toEqual([]); 287 + expect(json.verification).toBeNull(); 288 + expect(json.effectivePolicy).toBeNull(); 289 + }); 290 + 291 + it("returns nulls for an untracked DID", async () => { 292 + const res = await authGet( 293 + app, 294 + "/xrpc/org.p2pds.admin.getDidStatus?did=did:plc:unknown", 295 + ); 296 + expect(res.status).toBe(200); 297 + 298 + const json = await res.json() as Record<string, unknown>; 299 + expect(json.did).toBe("did:plc:unknown"); 300 + expect(json.syncState).toBeNull(); 301 + expect(json.blockCount).toBe(0); 302 + expect(json.blobCount).toBe(0); 303 + }); 304 + }); 305 + 306 + // ============================================ 307 + // getNetworkStatus 308 + // ============================================ 309 + 310 + describe("Admin: getNetworkStatus", () => { 311 + let tmpDir: string; 312 + let db: InstanceType<typeof Database>; 313 + 314 + beforeEach(() => { 315 + tmpDir = mkdtempSync(join(tmpdir(), "admin-network-test-")); 316 + db = new Database(join(tmpDir, "test.db")); 317 + }); 318 + 319 + afterEach(() => { 320 + db.close(); 321 + try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} 322 + }); 323 + 324 + it("returns empty state without networking", async () => { 325 + const config = testConfig(tmpDir); 326 + const repoManager = new RepoManager(db, config); 327 + repoManager.init(); 328 + const firehose = new Firehose(repoManager); 329 + const app = createApp(config, repoManager, firehose); 330 + 331 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getNetworkStatus"); 332 + expect(res.status).toBe(200); 333 + 334 + const json = await res.json() as Record<string, unknown>; 335 + expect(json.peerId).toBeNull(); 336 + expect(json.multiaddrs).toEqual([]); 337 + expect(json.connections).toBe(0); 338 + }); 339 + 340 + it("returns peer info with networking", async () => { 341 + const config = testConfig(tmpDir); 342 + const repoManager = new RepoManager(db, config); 343 + repoManager.init(); 344 + 345 + const mockNetworkService: NetworkService = { 346 + provideBlocks: async () => {}, 347 + publishCommitNotification: async () => {}, 348 + onCommitNotification: () => {}, 349 + subscribeCommitTopics: () => {}, 350 + unsubscribeCommitTopics: () => {}, 351 + getPeerId: () => "12D3KooWNetwork", 352 + getMultiaddrs: () => ["/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"], 353 + getConnectionCount: () => 7, 354 + getRemoteAddrs: () => [], 355 + publishIdentityNotification: async () => {}, 356 + onIdentityNotification: () => {}, 357 + subscribeIdentityTopics: () => {}, 358 + unsubscribeIdentityTopics: () => {}, 359 + }; 360 + 361 + const firehose = new Firehose(repoManager); 362 + const app = createApp( 363 + config, 364 + repoManager, 365 + firehose, 366 + undefined, 367 + mockNetworkService, 368 + ); 369 + 370 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getNetworkStatus"); 371 + expect(res.status).toBe(200); 372 + 373 + const json = await res.json() as Record<string, unknown>; 374 + expect(json.peerId).toBe("12D3KooWNetwork"); 375 + expect(json.multiaddrs).toEqual(["/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/udp/4001/quic-v1"]); 376 + expect(json.connections).toBe(7); 377 + }); 378 + }); 379 + 380 + // ============================================ 381 + // getPolicies 382 + // ============================================ 383 + 384 + describe("Admin: getPolicies", () => { 385 + let tmpDir: string; 386 + let db: InstanceType<typeof Database>; 387 + 388 + beforeEach(() => { 389 + tmpDir = mkdtempSync(join(tmpdir(), "admin-policies-test-")); 390 + db = new Database(join(tmpDir, "test.db")); 391 + }); 392 + 393 + afterEach(() => { 394 + db.close(); 395 + try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} 396 + }); 397 + 398 + it("returns disabled when no replication manager", async () => { 399 + const config = testConfig(tmpDir); 400 + const repoManager = new RepoManager(db, config); 401 + repoManager.init(); 402 + const firehose = new Firehose(repoManager); 403 + const app = createApp(config, repoManager, firehose); 404 + 405 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getPolicies"); 406 + expect(res.status).toBe(200); 407 + 408 + const json = await res.json() as Record<string, unknown>; 409 + expect(json.enabled).toBe(false); 410 + expect(json.policySet).toBeNull(); 411 + expect(json.explicitDids).toEqual([]); 412 + }); 413 + 414 + it("returns disabled when replication has no policy engine", async () => { 415 + const config = testConfig(tmpDir, ["did:plc:tracked1"]); 416 + const repoManager = new RepoManager(db, config); 417 + repoManager.init(); 418 + 419 + const mockNetworkService: NetworkService = { 420 + provideBlocks: async () => {}, 421 + publishCommitNotification: async () => {}, 422 + onCommitNotification: () => {}, 423 + subscribeCommitTopics: () => {}, 424 + unsubscribeCommitTopics: () => {}, 425 + getPeerId: () => null, 426 + getMultiaddrs: () => [], 427 + getConnectionCount: () => 0, 428 + getRemoteAddrs: () => [], 429 + publishIdentityNotification: async () => {}, 430 + onIdentityNotification: () => {}, 431 + subscribeIdentityTopics: () => {}, 432 + unsubscribeIdentityTopics: () => {}, 433 + }; 434 + 435 + const replicationManager = new ReplicationManager( 436 + db, 437 + config, 438 + repoManager, 439 + { putBlock: async () => {}, getBlock: async () => null, hasBlock: async () => false, putBlocks: async () => {} }, 440 + mockNetworkService, 441 + { resolve: async () => null } as any, 442 + ); 443 + replicationManager.getSyncStorage().initSchema(); 444 + 445 + const firehose = new Firehose(repoManager); 446 + const app = createApp( 447 + config, 448 + repoManager, 449 + firehose, 450 + undefined, 451 + mockNetworkService, 452 + undefined, 453 + replicationManager, 454 + ); 455 + 456 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getPolicies"); 457 + expect(res.status).toBe(200); 458 + 459 + const json = await res.json() as Record<string, unknown>; 460 + expect(json.enabled).toBe(false); 461 + }); 462 + 463 + it("returns policies when policy engine is configured", async () => { 464 + const config = testConfig(tmpDir, ["did:plc:tracked1"]); 465 + const repoManager = new RepoManager(db, config); 466 + repoManager.init(); 467 + 468 + const policySet: PolicySet = { 469 + version: 1, 470 + policies: [ 471 + { 472 + id: "mutual-aid", 473 + name: "Mutual Aid", 474 + target: { type: "list", dids: ["did:plc:friend1", "did:plc:friend2"] }, 475 + replication: { minCopies: 2 }, 476 + sync: { intervalSec: 300 }, 477 + retention: { maxAgeSec: 0, keepHistory: false }, 478 + priority: 50, 479 + enabled: true, 480 + }, 481 + ], 482 + }; 483 + const policyEngine = new PolicyEngine(policySet); 484 + 485 + const mockNetworkService: NetworkService = { 486 + provideBlocks: async () => {}, 487 + publishCommitNotification: async () => {}, 488 + onCommitNotification: () => {}, 489 + subscribeCommitTopics: () => {}, 490 + unsubscribeCommitTopics: () => {}, 491 + getPeerId: () => null, 492 + getMultiaddrs: () => [], 493 + getConnectionCount: () => 0, 494 + getRemoteAddrs: () => [], 495 + publishIdentityNotification: async () => {}, 496 + onIdentityNotification: () => {}, 497 + subscribeIdentityTopics: () => {}, 498 + unsubscribeIdentityTopics: () => {}, 499 + }; 500 + 501 + const replicationManager = new ReplicationManager( 502 + db, 503 + config, 504 + repoManager, 505 + { putBlock: async () => {}, getBlock: async () => null, hasBlock: async () => false, putBlocks: async () => {} }, 506 + mockNetworkService, 507 + { resolve: async () => null } as any, 508 + undefined, 509 + undefined, 510 + policyEngine, 511 + ); 512 + replicationManager.getSyncStorage().initSchema(); 513 + 514 + const firehose = new Firehose(repoManager); 515 + const app = createApp( 516 + config, 517 + repoManager, 518 + firehose, 519 + undefined, 520 + mockNetworkService, 521 + undefined, 522 + replicationManager, 523 + ); 524 + 525 + const res = await authGet(app, "/xrpc/org.p2pds.admin.getPolicies"); 526 + expect(res.status).toBe(200); 527 + 528 + const json = await res.json() as Record<string, unknown>; 529 + expect(json.enabled).toBe(true); 530 + expect(json.explicitDids).toEqual(["did:plc:friend1", "did:plc:friend2"]); 531 + const ps = json.policySet as PolicySet; 532 + expect(ps.version).toBe(1); 533 + expect(ps.policies).toHaveLength(1); 534 + expect(ps.policies[0]!.id).toBe("mutual-aid"); 535 + }); 536 + });
+143
src/xrpc/admin.ts
··· 1 + import type { Context } from "hono"; 2 + import type { AuthedAppEnv } from "../types.js"; 3 + import type { ReplicationManager } from "../replication/replication-manager.js"; 4 + import type { NetworkService } from "../ipfs.js"; 5 + 6 + const VERSION = "0.1.0"; 7 + 8 + export function getOverview( 9 + c: Context<AuthedAppEnv>, 10 + networkService: NetworkService | undefined, 11 + replicationManager: ReplicationManager | undefined, 12 + ): Response { 13 + const network = networkService 14 + ? { 15 + peerId: networkService.getPeerId(), 16 + multiaddrs: networkService.getMultiaddrs(), 17 + connections: networkService.getConnectionCount(), 18 + } 19 + : null; 20 + 21 + let replication: Record<string, unknown> | null = null; 22 + let firehose: Record<string, unknown> | null = null; 23 + let policy: Record<string, unknown> | null = null; 24 + let verification: { results: unknown[] } = { results: [] }; 25 + 26 + if (replicationManager) { 27 + const syncStates = replicationManager.getSyncStates(); 28 + replication = { 29 + enabled: true, 30 + trackedDids: syncStates.map((s) => s.did), 31 + syncStates, 32 + }; 33 + 34 + firehose = replicationManager.getFirehoseStats() ?? null; 35 + 36 + const policyEngine = replicationManager.getPolicyEngine(); 37 + if (policyEngine) { 38 + policy = { 39 + policyCount: policyEngine.getPolicies().length, 40 + explicitDids: policyEngine.getExplicitDids(), 41 + }; 42 + } 43 + 44 + const verificationResults = replicationManager.getVerificationResults(); 45 + verification = { 46 + results: Array.from(verificationResults.values()), 47 + }; 48 + } else { 49 + replication = { enabled: false, trackedDids: [], syncStates: [] }; 50 + } 51 + 52 + return c.json({ 53 + version: VERSION, 54 + did: c.env.DID, 55 + network, 56 + replication, 57 + firehose, 58 + policy, 59 + verification, 60 + }); 61 + } 62 + 63 + export function getDidStatus( 64 + c: Context<AuthedAppEnv>, 65 + replicationManager: ReplicationManager | undefined, 66 + ): Response { 67 + const did = c.req.query("did"); 68 + if (!did) { 69 + return c.json( 70 + { error: "MissingParameter", message: "did is required" }, 71 + 400, 72 + ); 73 + } 74 + 75 + if (!replicationManager) { 76 + return c.json( 77 + { error: "ReplicationNotEnabled", message: "Replication is not enabled" }, 78 + 400, 79 + ); 80 + } 81 + 82 + const syncStorage = replicationManager.getSyncStorage(); 83 + const syncState = syncStorage.getState(did); 84 + const blockCount = syncStorage.getBlockCount(did); 85 + const blobCount = syncStorage.getBlobCount(did); 86 + const peerEndpoints = syncStorage.getPeerEndpoints(did); 87 + 88 + const verificationResults = replicationManager.getVerificationResults(); 89 + const verification = verificationResults.get(did) ?? null; 90 + 91 + const policyEngine = replicationManager.getPolicyEngine(); 92 + const effectivePolicy = policyEngine ? policyEngine.evaluate(did) : null; 93 + 94 + return c.json({ 95 + did, 96 + syncState, 97 + effectiveSyncIntervalMs: replicationManager.getEffectiveSyncIntervalMs(did), 98 + blockCount, 99 + blobCount, 100 + peerEndpoints, 101 + verification, 102 + effectivePolicy, 103 + }); 104 + } 105 + 106 + export function getNetworkStatus( 107 + c: Context<AuthedAppEnv>, 108 + networkService: NetworkService | undefined, 109 + ): Response { 110 + if (!networkService) { 111 + return c.json({ 112 + peerId: null, 113 + multiaddrs: [], 114 + connections: 0, 115 + }); 116 + } 117 + 118 + return c.json({ 119 + peerId: networkService.getPeerId(), 120 + multiaddrs: networkService.getMultiaddrs(), 121 + connections: networkService.getConnectionCount(), 122 + }); 123 + } 124 + 125 + export function getPolicies( 126 + c: Context<AuthedAppEnv>, 127 + replicationManager: ReplicationManager | undefined, 128 + ): Response { 129 + if (!replicationManager) { 130 + return c.json({ enabled: false, policySet: null, explicitDids: [] }); 131 + } 132 + 133 + const policyEngine = replicationManager.getPolicyEngine(); 134 + if (!policyEngine) { 135 + return c.json({ enabled: false, policySet: null, explicitDids: [] }); 136 + } 137 + 138 + return c.json({ 139 + enabled: true, 140 + policySet: policyEngine.export(), 141 + explicitDids: policyEngine.getExplicitDids(), 142 + }); 143 + }