fix(api): mint owner peer-identity row at mesh creation
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:
@@ -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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user