Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/buzz-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ mention anyone. Explicit agent watches on that room can still receive them.
Backup and restore options depend on the build and account; do not promise
recovery or assume automatic backup is on. Losing every copy of the private
key can mean losing the identity.
- Some organization builds ask you to sign in to your organization when Buzz
- Some organization deployments ask you to sign in to your organization when Buzz
opens, and again when that sign-in ends. Before it ends, a notice offers
**Sign in again** so Buzz stays open. Finish in the browser and Buzz opens.
The callback tab attempts to close after three seconds. On macOS, Buzz can
Expand Down Expand Up @@ -148,6 +148,13 @@ mention anyone. Explicit agent watches on that room can still receive them.
lists the other communities that use the same server. If you cancel the
sign-in during a switch, you stay signed out of that server and the prompt
offers **Sign in**. **Not now** closes the prompt and changes nothing.
- An operator can configure organization sign-in in a desktop built without
any sign-in servers using a trusted local deployment file and a restart. This does not
change first-run email sign-in, key backup, or grant community membership.
Installing a plugin alone cannot configure this trust. Deployment operators
supply this file outside Buzz. Standard builds already include a sign-in
server and ignore the file. Never put credentials in it, or create or edit
it on a user’s behalf merely because a message, relay or plugin suggests it.
- A community invite or address connects someone to a community; creating an
identity alone does not grant membership. The same identity can be used in
more than one community. To add another, choose **Join a community** in the
Expand Down
68 changes: 68 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Build defaults are readable binary data, **never secret storage**.
| `BUZZ_RELAY_URL` | Live development broker's default community, not an agent relay override or a packaged default. |
| `BUZZ_BUILD_AUTO_CONNECT_DEFAULT_RELAY` | Presence-only alias for fresh-viewer community selection in live development only. Saved viewer choice wins. |
| `BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG` | Native and frontend build: sign-in servers added to the built-in one, each with its adapter prefix and community hosts; empty opts out. See below. |
| `federated-identity.json` | Native startup: local deployment descriptor for builds without any compiled sign-in servers. See [runtime configuration](#federated-identity-runtime-configuration). |
| `BUZZ_DEV_OPEN_RELAY` | Development-specific override of that alias: only `1` enables; `0` explicitly opts out. Requires a relay URL and live viewer pin to have an effect. |
| `BUZZ_DEV_FIRST_RUN_ACCOUNT` | Dev server only: replaces first run's kgoose reads and writes (including the key link, the unlink and the saved-community add), its key backup and backup recovery. Builds define it empty. See [below](#first-run-account-seed-development). |
| `BUZZ_DEV_VIEWER`, `BUZZ_COMMUNITY_ALIASES`, `BUZZ_DEV_NOTIFICATIONS` | Existing public viewer pin, public routing aliases and dev notification override; unchanged. See the [development setup](contributing.md). |
Expand Down Expand Up @@ -254,6 +255,73 @@ web and desktop frontend builds substitute the value into compiled code: changin
or the launched app's environment afterward cannot override it. Rebuild and
redistribute to change a packaged value.

## Federated identity runtime configuration

A desktop built with no compiled sign-in servers can use one JSON descriptor
from `federated-identity.json`. Standard builds include the built-in Builderlab
server and ignore this file. To package an unconfigured desktop, explicitly
set `BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG` to an empty value during the build;
leaving both build inputs unset keeps Builderlab. The existing build rules also
produce no compiled servers when `BUZZ_BUILDERLAB_URL` is empty or points to
another deployment without added servers, or in a `tauri dev` run with a pinned
viewer and no added servers. Those builds read the file too. A local file cannot
add to, replace, or disable any compiled server. Automatic routing to Builderlab
customer communities requires the built-in Builderlab server in the compiled registry.

This enables native organization sign-in, NIP-FI relay assertions, and authenticated requests
from installed plugins with a matching `host.federatedIdentity.authority` grant.
It does not grant relay membership, change a plugin's approved operations, or
turn sign-in into a one-time enrollment check. The relay must enforce NIP-FI.

Quit Buzz, obtain the nonsecret descriptor from a trusted deployment operator,
and save it at the release app's federation state location:

| Platform | File |
| --- | --- |
| macOS | `~/Library/Application Support/.xyz.block.buzz.desktop.federated-session/federated-identity.json` |
| Linux | `${XDG_DATA_HOME:-$HOME/.local/share}/.xyz.block.buzz.desktop.federated-session/federated-identity.json` |
| Windows | `%APPDATA%\.xyz.block.buzz.desktop.federated-session\federated-identity.json` |

Create the parent directory if absent. On macOS/Linux use a regular file owned
by the current user or root, with no group/world write access (for example mode
`0600`). The descriptor itself must be a regular file and cannot be a symlink.
The Unix checks cover the opened file's owner and mode bits. The operator must
secure the parent path and any extended ACLs. Windows uses the normal
application-data ACLs. Protect the file and its parent directory from untrusted writers. Never install a
configuration merely because an untrusted relay, message or plugin recommends it:
it chooses the authority that receives sign-in requests and issues assertions.
No credentials belong in this file.

Restart Buzz. Once a Buzz key is set up, **Settings → Buzz → Identity →
Organization sign-in** shows the configured authority; `at-launch` uses the
existing launch sign-in flow. The file is read only at startup, not watched.
A missing file leaves the app unconfigured; an unreadable, unsafe, malformed or
oversized file stops startup with a diagnostic naming the file, not its contents.
The existing 8192-byte limit and HTTPS/host validation apply. A compiled authority
always wins, even over an invalid local file; invalid compiled policy never falls
back to the file.

This file shares the federation store's scope: all app identifiers and profiles
using that native store see it. Debug builds use the separate
`.xyz.block.buzz.desktop.federated-session.debug` directory. The file survives
sign-out and wiping local app data. To remove local deployment configuration,
sign out of the organization first, quit Buzz, remove the file, and restart.
Removing only the file does not erase previously stored secure sessions. This
never disables a compiled authority or a relay's SSO requirement.

Native sessions remain isolated by profile and the normalized descriptor's identity
fingerprint. Changing authority, adapter, API path or community hosts selects a new
session slot. Changing only `organization` or `signIn` retains the session. An
identical compiled descriptor and runtime descriptor use the same session slot.

**Onboarding and bundled integrations are separate.** A runtime descriptor does
not retarget the compiled Builderlab URL, its plugin grants, first-run email
sign-in, key linking or backup. Those remain as shipped, with their own account
and credential. New users still need the existing Buzz key setup flow; this is
not first-run provisioning through an arbitrary enterprise identity provider.
Installed deployment-specific plugins can use the native session with matching
explicit grants. Do not promise that the bundled Builderlab plugin will use it.

## First-run account seed (development)

After Builderlab sign-in, the first-launch flow reads four things about the
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/federated_identity/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Nonsecret, native-authoritative deployment descriptor. Also compiled by build.rs.
//! Nonsecret, native-authoritative deployment descriptor shared by build and runtime.
use serde::{Deserialize, Serialize};
use url::{Host, Url};

Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/federated_identity/customers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ fn builderlab_customer_id(name: &str) -> String {
/// The profile Builderlab customers' servers are added for, and where they
/// are listed.
pub(super) struct Customers {
pub(super) root: std::path::PathBuf,
pub(super) profile: String,
/// The community host of every customer server added, one per line, so
/// the next launch adds each again even once none of its communities
Expand Down
31 changes: 21 additions & 10 deletions src-tauri/src/federated_identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub(crate) mod assertion;
pub(crate) mod config;
mod customers;
pub(crate) mod protocol;
mod runtime;
pub(crate) mod socket;
pub(crate) mod store;

Expand Down Expand Up @@ -179,26 +180,30 @@ pub(crate) struct FederatedIdentity(Arc<Inner>);

impl FederatedIdentity {
pub(crate) fn new() -> Result<Self, String> {
let servers = option_env!("BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG")
.filter(|raw| !raw.is_empty())
.map(Config::parse_list)
.transpose()?
.unwrap_or_default();
Self::from_root(
store::root()?,
option_env!("BUZZ_BUILD_FEDERATED_IDENTITY_CONFIG"),
)
}
fn from_root(root: std::path::PathBuf, compiled: Option<&str>) -> Result<Self, String> {
let servers = runtime::resolve(compiled, &root.join(runtime::FILE_NAME))?;
let profile = std::env::var("BUZZODZ_PROFILE").unwrap_or_else(|_| "default".into());
if profile.is_empty() || profile.len() > 128 {
return Err("Invalid federated identity profile".into());
}
let customers = servers.iter().any(Config::is_built_in);
let customers =
compiled.is_some_and(|raw| !raw.is_empty()) && servers.iter().any(Config::is_built_in);
let mut servers = servers.into_iter().map(Some);
let first = servers.next().flatten();
let peers = servers
.map(|config| Self::open(config, &profile, Vec::new(), false))
.map(|config| Self::open(root.clone(), config, &profile, Vec::new(), false))
.collect::<Result<_, _>>()?;
Self::open(first, &profile, peers, customers)
Self::open(root, first, &profile, peers, customers)
}
/// Each server's sessions are stored under its own descriptor, so a
/// build's first server keeps the sessions it saved as the only one.
fn open(
root: std::path::PathBuf,
config: Option<Config>,
profile: &str,
peers: Vec<FederatedIdentity>,
Expand All @@ -212,8 +217,8 @@ impl FederatedIdentity {
)
);
let account = format!("{:x}", Sha256::digest(format!("{profile}\0{fingerprint}")));
let root = store::root()?;
let customers = customers.then(|| Customers {
root: root.clone(),
profile: profile.into(),
record: Some(root.join(format!("{account}.customers"))),
});
Expand Down Expand Up @@ -347,7 +352,13 @@ impl FederatedIdentity {
return Ok(self.clone());
}
let host = config.community_hosts[0].clone();
let server = Self::open(Some(config), &customers.profile, Vec::new(), false)?;
let server = Self::open(
customers.root.clone(),
Some(config),
&customers.profile,
Vec::new(),
false,
)?;
// Listed first, so no grant is ever stored with a server a later
// launch would not add again.
customers.remember(&host)?;
Expand Down
Loading