feat(broker+api+cli): per-topic E2E encryption — v0.3.0 phase 3 (CLI)
Wire format:
topic_member_key.encrypted_key = base64(
<32-byte sender x25519 pubkey> || crypto_box(topic_key)
)
Embedding sender pubkey inline lets re-sealed copies (carrying a
different sender than the original creator-seal) decode the same
way as creator copies, without an extra schema column or join.
topic.encrypted_key_pubkey stays for backwards-compat metadata
but the wire truth is the inline prefix.
API (phase 3):
GET /v1/topics/:name/pending-seals list members without keys
POST /v1/topics/:name/seal submit a re-sealed copy
POST /v1/messages now accepts bodyVersion (1|2); v2 skips the
regex mention extraction (server can't read v2 ciphertext).
GET /messages + /stream now return bodyVersion per row.
Broker + web mutations updated to use the inline-sender format
when sealing. ensureGeneralTopic (web) also generates topic keys
per the bugfix that landed earlier today; both producers now
share one wire format.
CLI (claudemesh-cli@1.8.0):
+ apps/cli/src/services/crypto/topic-key.ts — fetch/decrypt/encrypt/seal
+ claudemesh topic post <name> <msg> — encrypted REST send (v2)
* claudemesh topic tail <name> — decrypts v2 on render, runs a
30s background re-seal loop for pending joiners
Web client stays on v1 plaintext until phase 3.5 (browser-side
persistent identity in IndexedDB). Mention fan-out from phase 1
already works for both versions, so /v1/notifications keeps
working through the cutover.
Spec at .artifacts/specs/2026-05-02-topic-key-onboarding.md
updated with the implemented inline-sender format and the
phase 3.5 web plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -120,7 +120,8 @@ Topic (conversation scope, v0.2.0)
|
||||
claudemesh topic history <t> fetch message history [--limit --before]
|
||||
claudemesh topic read <topic> mark all as read
|
||||
claudemesh topic tail <topic> live SSE tail [--limit --forward-only]
|
||||
claudemesh send "#topic" "msg" send to a topic
|
||||
claudemesh topic post <t> <msg> encrypted REST post (v0.3.0 v2)
|
||||
claudemesh send "#topic" "msg" send to a topic (WS path, v1 plaintext)
|
||||
claudemesh member list mesh roster with online state [--online]
|
||||
claudemesh notification list recent @-mentions of you [--since <ISO>]
|
||||
|
||||
@@ -586,7 +587,17 @@ async function main(): Promise<void> {
|
||||
const { runTopicTail } = await import("~/commands/topic-tail.js");
|
||||
process.exit(await runTopicTail(arg, tailFlags));
|
||||
}
|
||||
else { console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read|tail>"); process.exit(EXIT.INVALID_ARGS); }
|
||||
else if (sub === "post") {
|
||||
const postFlags = {
|
||||
mesh: flags.mesh as string,
|
||||
json: !!flags.json,
|
||||
plaintext: !!flags.plaintext,
|
||||
};
|
||||
const message = positionals.slice(2).join(" ");
|
||||
const { runTopicPost } = await import("~/commands/topic-post.js");
|
||||
process.exit(await runTopicPost(arg, message, postFlags));
|
||||
}
|
||||
else { console.error("Usage: claudemesh topic <create|list|join|leave|members|history|read|tail|post>"); process.exit(EXIT.INVALID_ARGS); }
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user