Compare commits
2 Commits
39929eb7fe
...
82ebd2b6be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82ebd2b6be | ||
|
|
b70536195a |
@@ -38,7 +38,6 @@ import {
|
|||||||
meshFileKey,
|
meshFileKey,
|
||||||
meshContext,
|
meshContext,
|
||||||
meshMember as memberTable,
|
meshMember as memberTable,
|
||||||
meshMember,
|
|
||||||
meshMemory,
|
meshMemory,
|
||||||
meshNotification,
|
meshNotification,
|
||||||
meshState,
|
meshState,
|
||||||
@@ -896,12 +895,12 @@ async function fanOutMentions(args: {
|
|||||||
|
|
||||||
const recipients = await db
|
const recipients = await db
|
||||||
.select({
|
.select({
|
||||||
id: meshMember.id,
|
id: memberTable.id,
|
||||||
displayName: meshMember.displayName,
|
displayName: memberTable.displayName,
|
||||||
})
|
})
|
||||||
.from(meshMember)
|
.from(memberTable)
|
||||||
.where(
|
.where(
|
||||||
and(eq(meshMember.meshId, topic.meshId), isNull(meshMember.revokedAt)),
|
and(eq(memberTable.meshId, topic.meshId), isNull(memberTable.revokedAt)),
|
||||||
);
|
);
|
||||||
const tokenSet = new Set(tokens);
|
const tokenSet = new Set(tokens);
|
||||||
const targets = recipients
|
const targets = recipients
|
||||||
|
|||||||
@@ -1952,6 +1952,7 @@ async function handleSend(
|
|||||||
senderSessionPubkey: conn.sessionPubkey ?? undefined,
|
senderSessionPubkey: conn.sessionPubkey ?? undefined,
|
||||||
nonce: msg.nonce,
|
nonce: msg.nonce,
|
||||||
ciphertext: msg.ciphertext,
|
ciphertext: msg.ciphertext,
|
||||||
|
mentions: msg.mentions,
|
||||||
}).catch((e) =>
|
}).catch((e) =>
|
||||||
log.warn("appendTopicMessage failed", { topic_id: topicId, err: String(e) }),
|
log.warn("appendTopicMessage failed", { topic_id: topicId, err: String(e) }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ export interface WSSendMessage {
|
|||||||
nonce: string; // base64
|
nonce: string; // base64
|
||||||
ciphertext: string; // base64
|
ciphertext: string; // base64
|
||||||
id?: string; // client-side correlation id
|
id?: string; // client-side correlation id
|
||||||
|
/**
|
||||||
|
* Optional client-extracted `@-mention` display names (lowercased,
|
||||||
|
* no `@` prefix, max 16). Required when `body_version: 2` cipher
|
||||||
|
* lands in v0.3.0 phase 3 — the server can't read v2 ciphertext to
|
||||||
|
* regex-match. Today's v1 plaintext path falls back to a regex on
|
||||||
|
* the body when this is absent.
|
||||||
|
*/
|
||||||
|
mentions?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Broker → client: an envelope addressed to this peer. */
|
/** Broker → client: an envelope addressed to this peer. */
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { randomBytes } from "node:crypto";
|
|||||||
|
|
||||||
import sodium from "libsodium-wrappers";
|
import sodium from "libsodium-wrappers";
|
||||||
|
|
||||||
import { and, eq, isNull } from "@turbostarter/db";
|
import { and, asc, eq, isNull } from "@turbostarter/db";
|
||||||
import {
|
import {
|
||||||
invite,
|
invite,
|
||||||
mesh,
|
mesh,
|
||||||
meshMember,
|
meshMember,
|
||||||
meshTopic,
|
meshTopic,
|
||||||
meshTopicMember,
|
meshTopicMember,
|
||||||
|
meshTopicMemberKey,
|
||||||
pendingInvite,
|
pendingInvite,
|
||||||
} from "@turbostarter/db/schema";
|
} from "@turbostarter/db/schema";
|
||||||
import { db } from "@turbostarter/db/server";
|
import { db } from "@turbostarter/db/server";
|
||||||
@@ -206,11 +207,24 @@ export const ensureGeneralTopic = async (
|
|||||||
meshId: string,
|
meshId: string,
|
||||||
): Promise<{ id: string } | null> => {
|
): Promise<{ id: string } | null> => {
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select({ id: meshTopic.id })
|
.select({
|
||||||
|
id: meshTopic.id,
|
||||||
|
encryptedKeyPubkey: meshTopic.encryptedKeyPubkey,
|
||||||
|
})
|
||||||
.from(meshTopic)
|
.from(meshTopic)
|
||||||
.where(and(eq(meshTopic.meshId, meshId), eq(meshTopic.name, "general")))
|
.where(and(eq(meshTopic.meshId, meshId), eq(meshTopic.name, "general")))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (existing) return existing;
|
if (existing) return { id: existing.id };
|
||||||
|
|
||||||
|
// Generate the topic's symmetric key + an ephemeral sender keypair
|
||||||
|
// for v0.3.0 phase 2 sealing. Mirrors the broker's createTopic path
|
||||||
|
// so web-created topics aren't stuck as unencrypted v0.2.0 placeholders.
|
||||||
|
// The plaintext topicKey leaves memory after sealing one copy for
|
||||||
|
// the mesh owner — the broker never persists it.
|
||||||
|
await sodium.ready;
|
||||||
|
const topicKey = sodium.randombytes_buf(32);
|
||||||
|
const senderKp = sodium.crypto_box_keypair();
|
||||||
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.insert(meshTopic)
|
.insert(meshTopic)
|
||||||
.values({
|
.values({
|
||||||
@@ -218,10 +232,49 @@ export const ensureGeneralTopic = async (
|
|||||||
name: "general",
|
name: "general",
|
||||||
description: "Default mesh-wide channel. Every member can read and post.",
|
description: "Default mesh-wide channel. Every member can read and post.",
|
||||||
visibility: "public",
|
visibility: "public",
|
||||||
|
encryptedKeyPubkey: sodium.to_hex(senderKp.publicKey),
|
||||||
})
|
})
|
||||||
.onConflictDoNothing()
|
.onConflictDoNothing()
|
||||||
.returning({ id: meshTopic.id });
|
.returning({ id: meshTopic.id });
|
||||||
return row ?? null;
|
if (!row) return null;
|
||||||
|
|
||||||
|
// Seal a copy for the oldest non-revoked member (the owner, by
|
||||||
|
// construction — owner-as-member rows are minted at mesh creation
|
||||||
|
// time, ahead of this call).
|
||||||
|
const [owner] = await db
|
||||||
|
.select({
|
||||||
|
id: meshMember.id,
|
||||||
|
peerPubkey: meshMember.peerPubkey,
|
||||||
|
})
|
||||||
|
.from(meshMember)
|
||||||
|
.where(and(eq(meshMember.meshId, meshId), isNull(meshMember.revokedAt)))
|
||||||
|
.orderBy(asc(meshMember.joinedAt))
|
||||||
|
.limit(1);
|
||||||
|
if (owner) {
|
||||||
|
try {
|
||||||
|
const recipientX25519 = sodium.crypto_sign_ed25519_pk_to_curve25519(
|
||||||
|
sodium.from_hex(owner.peerPubkey),
|
||||||
|
);
|
||||||
|
const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES);
|
||||||
|
const sealed = sodium.crypto_box_easy(
|
||||||
|
topicKey,
|
||||||
|
nonce,
|
||||||
|
recipientX25519,
|
||||||
|
senderKp.privateKey,
|
||||||
|
);
|
||||||
|
await db.insert(meshTopicMemberKey).values({
|
||||||
|
topicId: row.id,
|
||||||
|
memberId: owner.id,
|
||||||
|
encryptedKey: sodium.to_base64(sealed, sodium.base64_variants.ORIGINAL),
|
||||||
|
nonce: sodium.to_base64(nonce, sodium.base64_variants.ORIGINAL),
|
||||||
|
}).onConflictDoNothing();
|
||||||
|
} catch {
|
||||||
|
// Owner pubkey isn't a valid ed25519 key (legacy data?). Topic
|
||||||
|
// is still created — phase 3 re-seal flow will handle it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return row;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const archiveMyMesh = async ({
|
export const archiveMyMesh = async ({
|
||||||
|
|||||||
Reference in New Issue
Block a user