fix(api): mint owner peer-identity row at mesh creation
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled

Web-first owners had no mesh.member row because the broker only ever
created one on first WS hello (CLI flow). The topic chat page server
component requires that row to issue a dashboard apikey
(issuedByMemberId is a FK to mesh.member), so visiting the chat for a
web-only mesh hit notFound() on the owner's own room.

Forward fix: createMyMesh now generates a fresh ed25519 peer keypair,
inserts a mesh.member row with role=admin and dashboardUserId=userId,
and subscribes the owner to the auto-created #general topic as 'lead'.
The peer secret key is intentionally discarded — web users don't sign
anything in v0.2.0 (no DMs, base64 plaintext on topics). If the same
user later runs the CLI, the broker mints a separate member row from
its own keypair; both work for their respective surfaces.

Backfill: apps/broker/scripts/backfill-owner-members.ts walks every
non-archived mesh whose owner has no member row, generates real
ed25519 keypairs via libsodium, inserts the rows in a transaction,
and subscribes each as 'lead' on #general. Already run against prod
— 13 owner rows minted, ddtest verified end-to-end via playwriter
(send → poll → render round-trip ok).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-05-02 17:02:40 +01:00
parent 6de5e275fa
commit 2aa21fe07c
2 changed files with 138 additions and 3 deletions

View File

@@ -148,12 +148,46 @@ export const createMyMesh = async ({
rootKey,
})
.returning({ id: mesh.id, slug: mesh.slug });
if (!created) throw new Error("mesh insert returned no row");
if (created) {
await ensureGeneralTopic(created.id);
// Create the owner's peer-identity member row. Mirrors what the broker
// does on first WS hello so a web-only user has a valid identity from
// t=0 — without this, the topic chat can't issue a dashboard apikey
// (issuedByMemberId is a FK), and the owner's "oldest member row in
// the mesh" lookup returns null. Fresh ed25519 keypair; secret key is
// discarded because web users don't sign anything in v0.2.0 (no DMs,
// base64 plaintext on topics). If they later install the CLI, the
// broker will mint a separate member row with a CLI-side keypair —
// both work for their respective surfaces.
const peerKp = s.crypto_sign_keypair();
const peerPubkey = s.to_hex(peerKp.publicKey);
const [ownerMember] = await db
.insert(meshMember)
.values({
meshId: created.id,
peerPubkey,
displayName: `${input.name}-owner`,
role: "admin",
userId,
dashboardUserId: userId,
})
.returning({ id: meshMember.id });
if (!ownerMember) throw new Error("owner member insert returned no row");
// Auto-create #general and subscribe the owner as 'lead'.
const generalTopic = await ensureGeneralTopic(created.id);
if (generalTopic) {
await db
.insert(meshTopicMember)
.values({
topicId: generalTopic.id,
memberId: ownerMember.id,
role: "lead",
})
.onConflictDoNothing();
}
return created!;
return created;
};
/**