fix(broker): drained pushes carry sender identity + topic like live pushes (bug2)
maybePushQueuedMessages sent queued rows with only senderPubkey, so the daemon labelled them with an 8-char pubkey, couldn't tell a topic post from a DM, and couldn't run its own-member echo filter. drainForMember now returns senderMemberPubkey, senderName (session presence name, else the member's) and the topic name for `#<topicId>` targets; the push envelope forwards them plus senderMemberId. Covered by two new broker.test.ts cases (run against pgvector pg16). Spec: .artifacts/specs/2026-09-26-session-identity-and-routing.md §1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2456,6 +2456,10 @@ export async function drainForMember(
|
|||||||
/** v0.9.0 daemon fields; null for legacy traffic. */
|
/** v0.9.0 daemon fields; null for legacy traffic. */
|
||||||
clientMessageId: string | null;
|
clientMessageId: string | null;
|
||||||
requestFingerprint: string | null;
|
requestFingerprint: string | null;
|
||||||
|
/** 1.38.0: identity + topic, mirroring the live push envelope. */
|
||||||
|
senderMemberPubkey: string;
|
||||||
|
senderName: string | null;
|
||||||
|
topic: string | null;
|
||||||
}>
|
}>
|
||||||
> {
|
> {
|
||||||
const priorities = deliverablePriorities(status);
|
const priorities = deliverablePriorities(status);
|
||||||
@@ -2524,6 +2528,9 @@ export async function drainForMember(
|
|||||||
sender_pubkey: string;
|
sender_pubkey: string;
|
||||||
client_message_id: string | null;
|
client_message_id: string | null;
|
||||||
request_fingerprint: string | null;
|
request_fingerprint: string | null;
|
||||||
|
sender_member_pubkey: string;
|
||||||
|
sender_name: string | null;
|
||||||
|
topic_name: string | null;
|
||||||
}>(sql`
|
}>(sql`
|
||||||
WITH claimed AS (
|
WITH claimed AS (
|
||||||
UPDATE mesh.message_queue AS mq
|
UPDATE mesh.message_queue AS mq
|
||||||
@@ -2546,7 +2553,22 @@ export async function drainForMember(
|
|||||||
RETURNING mq.id, mq.priority, mq.nonce, mq.ciphertext,
|
RETURNING mq.id, mq.priority, mq.nonce, mq.ciphertext,
|
||||||
mq.created_at, mq.sender_member_id,
|
mq.created_at, mq.sender_member_id,
|
||||||
mq.client_message_id, mq.request_fingerprint,
|
mq.client_message_id, mq.request_fingerprint,
|
||||||
COALESCE(mq.sender_session_pubkey, m.peer_pubkey) AS sender_pubkey
|
COALESCE(mq.sender_session_pubkey, m.peer_pubkey) AS sender_pubkey,
|
||||||
|
-- 1.38.0 (spec 2026-09-26 §1): same identity fields the live
|
||||||
|
-- push carries, so a drained message renders with the right
|
||||||
|
-- author + topic instead of a bare 8-char pubkey.
|
||||||
|
m.peer_pubkey AS sender_member_pubkey,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT p.display_name FROM mesh.presence p
|
||||||
|
WHERE mq.sender_session_pubkey IS NOT NULL
|
||||||
|
AND p.session_pubkey = mq.sender_session_pubkey
|
||||||
|
AND p.display_name IS NOT NULL
|
||||||
|
ORDER BY p.connected_at DESC LIMIT 1),
|
||||||
|
m.display_name
|
||||||
|
) AS sender_name,
|
||||||
|
CASE WHEN mq.target_spec LIKE '#%'
|
||||||
|
THEN (SELECT t.name FROM mesh.topic t WHERE t.id = substring(mq.target_spec from 2))
|
||||||
|
END AS topic_name
|
||||||
)
|
)
|
||||||
SELECT * FROM claimed ORDER BY created_at ASC, id ASC
|
SELECT * FROM claimed ORDER BY created_at ASC, id ASC
|
||||||
`);
|
`);
|
||||||
@@ -2561,6 +2583,9 @@ export async function drainForMember(
|
|||||||
sender_pubkey: string;
|
sender_pubkey: string;
|
||||||
client_message_id: string | null;
|
client_message_id: string | null;
|
||||||
request_fingerprint: string | null;
|
request_fingerprint: string | null;
|
||||||
|
sender_member_pubkey: string;
|
||||||
|
sender_name: string | null;
|
||||||
|
topic_name: string | null;
|
||||||
}>;
|
}>;
|
||||||
if (!rows || rows.length === 0) return [];
|
if (!rows || rows.length === 0) return [];
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
@@ -2574,6 +2599,9 @@ export async function drainForMember(
|
|||||||
senderPubkey: r.sender_pubkey,
|
senderPubkey: r.sender_pubkey,
|
||||||
clientMessageId: r.client_message_id ?? null,
|
clientMessageId: r.client_message_id ?? null,
|
||||||
requestFingerprint: r.request_fingerprint ?? null,
|
requestFingerprint: r.request_fingerprint ?? null,
|
||||||
|
senderMemberPubkey: r.sender_member_pubkey,
|
||||||
|
senderName: r.sender_name ?? null,
|
||||||
|
topic: r.topic_name ?? null,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -731,6 +731,10 @@ async function maybePushQueuedMessages(
|
|||||||
messageId: m.id,
|
messageId: m.id,
|
||||||
meshId: conn.meshId,
|
meshId: conn.meshId,
|
||||||
senderPubkey: m.senderPubkey,
|
senderPubkey: m.senderPubkey,
|
||||||
|
senderMemberPubkey: m.senderMemberPubkey,
|
||||||
|
senderMemberId: m.senderMemberId,
|
||||||
|
...(m.senderName ? { senderName: m.senderName } : {}),
|
||||||
|
...(m.topic ? { topic: m.topic } : {}),
|
||||||
priority: m.priority,
|
priority: m.priority,
|
||||||
nonce: m.nonce,
|
nonce: m.nonce,
|
||||||
ciphertext: m.ciphertext,
|
ciphertext: m.ciphertext,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import { afterAll, afterEach, describe, expect, test } from "vitest";
|
import { afterAll, afterEach, describe, expect, test } from "vitest";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import { db } from "../src/db";
|
import { db } from "../src/db";
|
||||||
import { presence, pendingStatus } from "@turbostarter/db/schema/mesh";
|
import { meshTopic, meshTopicMember, presence, pendingStatus } from "@turbostarter/db/schema/mesh";
|
||||||
import {
|
import {
|
||||||
applyPendingHookStatus,
|
applyPendingHookStatus,
|
||||||
connectPresence,
|
connectPresence,
|
||||||
@@ -395,6 +395,58 @@ describe("targetSpec routing", () => {
|
|||||||
expect(drained[0]!.ciphertext).toBe("hi everyone");
|
expect(drained[0]!.ciphertext).toBe("hi everyone");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 1.38.0 (spec 2026-09-26 §1): a drained push carries the same identity
|
||||||
|
// + topic fields as a live push. Without them the daemon rendered
|
||||||
|
// «<8-char pubkey>: <binary>» and couldn't tell a topic post from a DM.
|
||||||
|
test("drained topic post carries topic name + sender identity", async () => {
|
||||||
|
m = await setupTestMesh("drain-identity");
|
||||||
|
const [t] = await db
|
||||||
|
.insert(meshTopic)
|
||||||
|
.values({ meshId: m.meshId, name: "compras-orq" })
|
||||||
|
.returning({ id: meshTopic.id });
|
||||||
|
await db.insert(meshTopicMember).values({ topicId: t!.id, memberId: m.peerB.memberId });
|
||||||
|
const sessionPk = "c".repeat(64);
|
||||||
|
await db.insert(presence).values({
|
||||||
|
memberId: m.peerA.memberId, sessionId: "s-a", sessionPubkey: sessionPk,
|
||||||
|
displayName: "Intra-Back-session-1", pid: 1, cwd: "/tmp",
|
||||||
|
});
|
||||||
|
await queueMessage({
|
||||||
|
meshId: m.meshId,
|
||||||
|
senderMemberId: m.peerA.memberId,
|
||||||
|
senderSessionPubkey: sessionPk,
|
||||||
|
targetSpec: `#${t!.id}`,
|
||||||
|
priority: "now",
|
||||||
|
nonce: "nt",
|
||||||
|
ciphertext: "sealed",
|
||||||
|
});
|
||||||
|
const drained = await drainForMember(m.meshId, m.peerB.memberId, m.peerB.pubkey, "idle");
|
||||||
|
expect(drained).toHaveLength(1);
|
||||||
|
expect(drained[0]).toMatchObject({
|
||||||
|
topic: "compras-orq",
|
||||||
|
senderPubkey: sessionPk,
|
||||||
|
senderMemberPubkey: m.peerA.pubkey,
|
||||||
|
senderName: "Intra-Back-session-1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("drained DM without session falls back to the member display name", async () => {
|
||||||
|
m = await setupTestMesh("drain-member-name");
|
||||||
|
await queueMessage({
|
||||||
|
meshId: m.meshId,
|
||||||
|
senderMemberId: m.peerA.memberId,
|
||||||
|
targetSpec: m.peerB.pubkey,
|
||||||
|
priority: "now",
|
||||||
|
nonce: "nd",
|
||||||
|
ciphertext: "dm",
|
||||||
|
});
|
||||||
|
const drained = await drainForMember(m.meshId, m.peerB.memberId, m.peerB.pubkey, "idle");
|
||||||
|
expect(drained[0]).toMatchObject({
|
||||||
|
topic: null,
|
||||||
|
senderMemberPubkey: m.peerA.pubkey,
|
||||||
|
senderName: "peer-a-drain-member-name",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("pubkey mismatch → message not drained", async () => {
|
test("pubkey mismatch → message not drained", async () => {
|
||||||
m = await setupTestMesh("pubkey-mismatch");
|
m = await setupTestMesh("pubkey-mismatch");
|
||||||
await queueMessage({
|
await queueMessage({
|
||||||
|
|||||||
Reference in New Issue
Block a user