Skip to content

web: voice notes — the community scribe posts the transcript [DCO rebuild, stacked on #7673] - #7674

Open
loviswaternakamoto wants to merge 6 commits into
block:mainfrom
skaists:voice-messages-v2
Open

loviswaternakamoto wants to merge 6 commits into
block:mainfrom
skaists:voice-messages-v2

Conversation

@loviswaternakamoto

Copy link
Copy Markdown

Rebuild of #7348 (DCO-fixed, stacked)

Supersedes #7348 — same changes, rebuilt stacked on #7673 (this feature rides the join-by-address phone room infrastructure, as before). Clean branch, signed commit range (the old branch carried an unsigned fork merge commit that failed DCO; history preserved, no force-push).


Stacks on #7311 (join-by-address) — the room view it extends. Only the last commit is new.

What

Voice notes for the phone room. Tap the mic, speak, tap again: the note goes to the community's own speech-to-text door, and the transcript is posted as the message, carrying the audio's sha256. The raw audio is deleted by the door after transcription — the room keeps text plus a digest, never a stored recording.

Proven live (the screenshot in the discussion): a cold phone joins by address alone, switches to #general, records ~15s of real Latvian speech (LibriVox, public domain), and the message arrives as

Šis ir LibriVox ieraksts. Visi LibriVox ierasti ir brīvi no autortiesībām. …
— 🎙 voice→text · lv · sha256:<digest of the deleted audio>

How

  • join-material.ts — optional voice: { path, tongues? } in join.json. Fail-closed both ways: absent → no mic is rendered (the capability is the operator's to declare, never the client's to assume); malformed → the whole material is refused, same law as rooms.
  • voice.ts — recorder (MediaRecorder, mime cascade webm/opus → mp4 → ogg) + the door POST: NIP-98 kind 27235, u tag signing the community's canonical origin while transport rides the road the stranger was given (the invite claim's law, applied), payload tag binding the sha256 of the raw audio bytes.
  • RoomView.tsx — mic in the composer (record → tap to send), tongue picker in the community's order, honest "transcribing…" state, plain-language refusals surfaced verbatim, the existing nsec guard applied to transcripts (defense in depth), recording abandoned on room switch, client-side stop at the door's 120s cap.

The server half is one file of community ops (node → ffmpeg → whisper-cli -l <tongue>, serialized, spool dir rm -rf'd in a finally) — relay untouched, so the client capability is the whole upstreamable surface. Language is pinned per request, never auto-detected; the reference deployment's tongue order is lv · th · ru · uk.

z3.2 seat and others added 6 commits September 15, 2026 18:29
…a phone and the relay address lands in the room

- web /join — paste the relay address; the community's pairing material (join.json, origin-relative, operator-gated: absent = plain refusal, never a guess) resolves the standing invite; a PERSISTENT browser-local key claims it (NIP-07 preferred whenever present, per the existing durable-membership gating); a phone-first room view opens on the live wire (NIP-42 AUTH → NIP-29 shape: kind 9, #h = channel id), history + composer, relay verdicts rendered verbatim, fail-closed throughout
- canonical-origin signing law: NIP-98 u tags and NIP-42 relay tags must name the community's CANONICAL origin as the relay declares it (NIP-11 /info push.origin), while transport rides the road the user was given — proven against an alias-host deployment (relay.skaists.dev behind skaists.buzz) where signing the alias is refused 401 URL-mismatch
- signer: optional persistent secretKey path (shared/lib/local-identity — generated once, stored locally, exportable as nsec, honestly labeled), so durable membership no longer REQUIRES an extension on phones that have none; requireNip07 flows unchanged
- checks: pnpm check (biome + file-size + pubkey-truncation) green, tsc clean, vite build clean
- proven LIVE on the estate hive (skaists.buzz / relay.skaists.dev, buzz 0.2.1 stock server): cold 390px phone context, no extension, address typed by hand → key made on the phone → standing invite claimed (role member) → NIP-42 ok on the alias road → room live, history rendered, message sent AND received; a relay with no published material refused in plain words

(cherry picked from commit 7605d1e)

Signed-off-by: loVis waTer <loviswater44@gmail.com>
Co-authored-by: zCode <zcode@skaists.dev>
…dings

RELAY GUARD (defense in depth): kind 9 (channel message) content carrying a bech32 SECRET KEY is refused before ingest — content_leaks_secret matches the token shape (nsec1 + >=15 bech32 digits, a real nsec is ~63) so ordinary words containing nsec1 inside them pass and only a real key shape trips it; plain-words refusal: "invalid: that looks like a private key (nsec1...) -- never send a secret into a room"; 3 unit tests (real token refused, normal + substring-trap + short-token + other-kind pass). FORK-TO-PROVEN LIVE on the throwaway stack (rotate-test.local, our debug binary, fresh owner+member): mint 200 -> claim 200 joined -> NIP-42 AUTH ok -> nsec post OK FALSE with the guard refusal -> GUARD_VERDICT=PROVEN.

CLIENT: the composer refuses the same token shape BEFORE signing, plain words, draft kept with a clear-the-draft action (a broadcast cannot be unsent); the substring trap sends normally (precision proven live on the estate hive); the copy-the-secret control moved OFF the composer into its own bottom sheet opened from the header (key), carrying the never-paste warning beside the copy button — the control that handles the secret is never next to the control that broadcasts.

ROOM SWITCHER: join.json gains optional rooms [{id,name}] (operator-curated; malformed rooms refuse the whole material — fail-closed); the phone view renders the rooms as chips under the header, switching resets the pane and re-opens the live socket on the chosen channel; the join lands on default_channel. Rooms proven live: welcome-everyone · general · huddle · PLUR, a message read in two rooms.

checks: cargo test -p buzz-relay --lib secret_guard 2/2, cargo build -p buzz-relay green (a [cfg(test)] attribute placement bug in the first cut was caught by cargo build vs cargo test divergence — the helper must sit OUTSIDE the tests mod); pnpm check + tsc + vite build green.

(cherry picked from commit 256b4b4)

Signed-off-by: loVis waTer <loviswater44@gmail.com>
Co-authored-by: zCode <zcode@skaists.dev>
…vent (kind 34550) + phone URL join

RELAY: KIND_COMMUNITY_JOIN_MATERIAL = 34550 (NIP-29's community-definition kind reused as the carrier) — admin-scoped to publish (Scope::AdminChannels: the community owner's key signs it, a member key cannot forge it), community-global, parameterized-replaceable by d tag. req.rs: the ONE unauthenticated read — a REQ whose every filter pins exactly kinds:[34550] with no other scoping and a small limit is served pre-AUTH (fail-closed shape guard, unit-tested: kindless/mixed/tagged/oversized/multi-filter all refuse); the query is pinned to global rows of that kind so no member data can appear on the path. This is what lets a stranger holding nothing but the wss:// URL join: no desktop, no QR, no join.json.

WEB: join-event.ts fetches the event off the wire (unauthenticated REQ, timeout, fail-closed parse with the same well-formedness law as join.json); JoinPage tries the WIRE FIRST and keeps join.json as the fallback for relays that have not published the event; data-join-source records which path served.

MOBILE: pairing 'Add Community' now accepts a wss:// URL (pair() dispatch) — join_by_address.dart ports the web protocol to the phone: fetch the event over the raw socket → mint the key IN-POCKET (nostr.Keys.generate — never asked 'do you have a key') → claim the standing invite over HTTP with the NIP-98 u tag naming the CANONICAL url while transporting on the pasted road (sign-the-identity, ride-the-road) → NIP-42 AUTH with the canonical origin in the relay tag must return OK true → Community stored against the canonical origin. Every refusal surfaces verbatim.

(cherry picked from commit 088a677)

Signed-off-by: loVis waTer <loviswater44@gmail.com>
Co-authored-by: zCode <zcode@skaists.dev>
…iew phase change) — the e2e asserts body[data-join-source]=event

(cherry picked from commit eeb2522)

Signed-off-by: loVis waTer <loviswater44@gmail.com>
Co-authored-by: zCode <zcode@skaists.dev>
…ld (rider to the order)

The relay's web /join view already joins a room by address (the same view
watch.html embeds verbatim in an iframe — same relay, same origin, same
localStorage identity: join once, every surface is the same member). So the
phone-app change is a LAUNCHER: pair() dispatches a pasted wss:// URL to
https://<host>/join/ via url_launcher (external browser — the identity the
flow mints lives at the relay origin, shared with watch.html and the door);
the /join view self-prefills when served by the relay, fetches the
owner-signed join material (kind 34550) off the wire, mints the key
on-device, and joins. The Dart protocol port (join_by_address.dart) is
REMOVED — superseded by the proven web flow it duplicated.

(cherry picked from commit 5dc7830)

Signed-off-by: loVis waTer <loviswater44@gmail.com>
Co-authored-by: zCode <zcode@skaists.dev>
…e message

A phone records a voice note in the room composer; the community's own
speech-to-text door (declared in join.json, fail-closed when absent)
transcribes it server-side (whisper.cpp, language PINNED per request,
never auto-detected) and the TRANSCRIPT is posted as the message,
carrying the audio's sha256. The raw audio is deleted by the door
after transcription — the room keeps text plus a digest, never a
recording.

- join-material: optional voice { path, tongues } — same fail-closed law
  as rooms (a malformed declaration refuses the material, never a guess)
- room composer: mic (record, tap again to send), tongue picker in the
  community's order (lv, th, ru, uk for skaists), honest transcribing
  state, plain-language refusals, the nsec guard applied to transcripts
- NIP-98 auth for the door: u tag signs the CANONICAL origin, payload
  tag binds the audio bytes, transport rides the given road — the invite
  claim's law, applied

(cherry picked from commit 0384a75)

Signed-off-by: loVis waTer <loviswater44@gmail.com>
Co-authored-by: zCode <zcode@skaists.dev>
@github-actions

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is b8aa0233f9e36afa1ea83b02ecc5b1ebd20cbee4...f6a10a59bb9da904196650d13c1cc846d0d17956.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review f6a10a59bb9da904196650d13c1cc846d0d17956 to authorize a new review.
Any previous review applies only to its recorded range.

@loviswaternakamoto

Copy link
Copy Markdown
Author

Note for reviewers — the security-review pipeline skips outside contributions until triggered. To run it over this exact range, one line from a block-org member:

@buzz-security-review f6a10a59bb9da904196650d13c1cc846d0d17956

(head sha for this PR: f6a10a59bb9da904196650d13c1cc846d0d17956)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant