feat(api): humans-as-peers in /v1/peers
Recently-active apikey holders (used in the last 5 minutes) appear in the peer list alongside WS-connected sessions. The dashboard chat user now becomes visible to CLI peers calling list_peers, closing the v1.6.0 humans-as-peers loop. Presence rows take precedence when both exist; rest-only rows get via:"rest" flag and idle status (no presence channel to infer working/dnd from). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import { z } from "zod";
|
|||||||
import { db } from "@turbostarter/db/server";
|
import { db } from "@turbostarter/db/server";
|
||||||
import {
|
import {
|
||||||
mesh,
|
mesh,
|
||||||
|
meshApiKey,
|
||||||
meshMember,
|
meshMember,
|
||||||
meshTopic,
|
meshTopic,
|
||||||
meshTopicMember,
|
meshTopicMember,
|
||||||
@@ -586,13 +587,22 @@ export const v1Router = new Hono<Env>()
|
|||||||
})
|
})
|
||||||
|
|
||||||
// GET /v1/peers — connected peers in the key's mesh
|
// GET /v1/peers — connected peers in the key's mesh
|
||||||
// Dedupe by memberId — a member can have multiple active presence
|
//
|
||||||
// rows (one per session). Status reflects the most recent presence;
|
// Sources, deduped by memberId:
|
||||||
// summary/groups come from the latest row.
|
// 1. presence rows — WS-connected peers (CLI sessions, MCP push-pipes)
|
||||||
|
// 2. recently-active apikey holders — humans driving the dashboard
|
||||||
|
// chat over REST. We treat any apikey used in the last 5 minutes
|
||||||
|
// as a live "human peer" so other CLIs can see them.
|
||||||
|
//
|
||||||
|
// Presence wins when both exist (more accurate status). Apikey-only
|
||||||
|
// rows get a `via: "rest"` flag and inherit the issuing member's
|
||||||
|
// identity — that's the only way the dashboard chat user appears in
|
||||||
|
// /list_peers from a CLI today.
|
||||||
.get("/peers", async (c) => {
|
.get("/peers", async (c) => {
|
||||||
const key = c.var.apiKey;
|
const key = c.var.apiKey;
|
||||||
requireCapability(key, "read");
|
requireCapability(key, "read");
|
||||||
const rows = await db
|
|
||||||
|
const presenceRows = await db
|
||||||
.select({
|
.select({
|
||||||
memberId: meshMember.id,
|
memberId: meshMember.id,
|
||||||
pubkey: meshMember.peerPubkey,
|
pubkey: meshMember.peerPubkey,
|
||||||
@@ -608,6 +618,28 @@ export const v1Router = new Hono<Env>()
|
|||||||
and(eq(meshMember.meshId, key.meshId), isNull(presence.disconnectedAt)),
|
and(eq(meshMember.meshId, key.meshId), isNull(presence.disconnectedAt)),
|
||||||
)
|
)
|
||||||
.orderBy(desc(presence.connectedAt));
|
.orderBy(desc(presence.connectedAt));
|
||||||
|
|
||||||
|
const restCutoff = new Date(Date.now() - 5 * 60 * 1000);
|
||||||
|
const restRows = await db
|
||||||
|
.select({
|
||||||
|
memberId: meshMember.id,
|
||||||
|
pubkey: meshMember.peerPubkey,
|
||||||
|
displayName: meshMember.displayName,
|
||||||
|
lastUsedAt: meshApiKey.lastUsedAt,
|
||||||
|
})
|
||||||
|
.from(meshApiKey)
|
||||||
|
.innerJoin(
|
||||||
|
meshMember,
|
||||||
|
eq(meshApiKey.issuedByMemberId, meshMember.id),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(meshApiKey.meshId, key.meshId),
|
||||||
|
isNull(meshApiKey.revokedAt),
|
||||||
|
gt(meshApiKey.lastUsedAt, restCutoff),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const peers: Array<{
|
const peers: Array<{
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
@@ -615,8 +647,10 @@ export const v1Router = new Hono<Env>()
|
|||||||
status: string;
|
status: string;
|
||||||
summary: string | null;
|
summary: string | null;
|
||||||
groups: unknown;
|
groups: unknown;
|
||||||
|
via: "ws" | "rest";
|
||||||
}> = [];
|
}> = [];
|
||||||
for (const r of rows) {
|
|
||||||
|
for (const r of presenceRows) {
|
||||||
if (seen.has(r.memberId)) continue;
|
if (seen.has(r.memberId)) continue;
|
||||||
seen.add(r.memberId);
|
seen.add(r.memberId);
|
||||||
peers.push({
|
peers.push({
|
||||||
@@ -625,6 +659,19 @@ export const v1Router = new Hono<Env>()
|
|||||||
status: r.status,
|
status: r.status,
|
||||||
summary: r.summary,
|
summary: r.summary,
|
||||||
groups: r.groups,
|
groups: r.groups,
|
||||||
|
via: "ws",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const r of restRows) {
|
||||||
|
if (seen.has(r.memberId)) continue;
|
||||||
|
seen.add(r.memberId);
|
||||||
|
peers.push({
|
||||||
|
pubkey: r.pubkey,
|
||||||
|
displayName: r.displayName,
|
||||||
|
status: "idle",
|
||||||
|
summary: null,
|
||||||
|
groups: [],
|
||||||
|
via: "rest",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return c.json({ peers });
|
return c.json({ peers });
|
||||||
|
|||||||
Reference in New Issue
Block a user