Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eb18afa
feat(projects): authorize home channel admins
johnmatthewtennant Aug 31, 2026
256d6b9
fix(db): renumber project revision migration
johnmatthewtennant Aug 31, 2026
68c42ee
fix(projects): allocate revision protocol kind
johnmatthewtennant Aug 31, 2026
d0542cc
fix(projects): harden revision lifecycle
johnmatthewtennant Aug 31, 2026
267244d
fix(projects): preserve revised channel membership
johnmatthewtennant Aug 31, 2026
9f48dd5
fix(projects): reset revisions across lifecycle changes
johnmatthewtennant Aug 31, 2026
a11c8de
fix(projects): gate channel creation on permissions
johnmatthewtennant Aug 31, 2026
7b20873
test(cli): register project channel commands
johnmatthewtennant Aug 31, 2026
cc209ae
fix(projects): satisfy current clippy
johnmatthewtennant Aug 31, 2026
367ec08
fix(db): move project revisions after main migrations
johnmatthewtennant Sep 1, 2026
adb6781
Merge remote-tracking branch 'origin/main' into jtennant/project-home…
johnmatthewtennant Sep 1, 2026
21087d8
test(db): include later write fences in parity
johnmatthewtennant Sep 1, 2026
dbfa466
fix(projects): preserve committed revision results
johnmatthewtennant Sep 1, 2026
d88016b
fix(projects): preserve channel management access
johnmatthewtennant Sep 1, 2026
ef90d63
fix(projects): scope revision reads and advance heads
johnmatthewtennant Sep 1, 2026
a0919b7
fix(projects): reject archived home authority
johnmatthewtennant Sep 1, 2026
b051e85
fix(projects): serialize channel authority reads
johnmatthewtennant Sep 1, 2026
ee41570
fix(projects): serialize channel state authority
johnmatthewtennant Sep 1, 2026
dc42d40
fix(projects): lock membership authority rows
johnmatthewtennant Sep 1, 2026
8e3516e
fix(projects): bound collaborative channel revisions
johnmatthewtennant Sep 1, 2026
1933ebf
Merge remote-tracking branch 'origin/main' into jtennant/project-home…
johnmatthewtennant Sep 1, 2026
4821ceb
fix(projects): reconcile collaborative revisions
johnmatthewtennant Sep 1, 2026
49fcc0a
test(projects): cover stale repository attachment
johnmatthewtennant Sep 1, 2026
6c63968
fix(db): box applied project revisions
johnmatthewtennant Sep 1, 2026
ce91102
fix(desktop): mock project revision heads in e2e
johnmatthewtennant Sep 1, 2026
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
4 changes: 2 additions & 2 deletions VISION_PROJECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ NIP-34 is the metadata and discovery layer. Git remains the transport. The trans

Real work spans repositories. The platform is a relay, a desktop app, and a mobile app — three repos, one project. Render one card per repo and they look like three unrelated things.

Grouping is the one forge semantic that per-repo tags cannot express, and it's worth being precise about why, because everything else here deliberately avoids a custom kind.
Grouping is the one forge container semantic that per-repo tags cannot express, and it's worth being precise about why, because everything else here deliberately avoids a custom container kind.

Put membership in each `kind:30617` and a project spanning Alice's and Bob's repos needs *both* of them to publish a tag naming the group. Alice can't enroll Bob's repo — she can't sign for his key. Cross-owner grouping becomes impossible, and the project's own name, description, and channel end up scattered across events with no single writer and no deletion story: dropping a repo from the group would mean editing an event you don't control.

So there is exactly one custom kind — [NIP-MP](docs/nips/NIP-MP.md), `kind:30621`. One signer, one replaceable event, all group state in one place:
So there is exactly one custom container kind — [NIP-MP](docs/nips/NIP-MP.md), `kind:30621`. One signer, one replaceable event, all group state in one place. NIP-MP also defines an actor-signed regular revision kind for the narrow case where authorized home-channel admins collaboratively manage related channels without holding the Project signer's key.

```json
{
Expand Down
44 changes: 37 additions & 7 deletions crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1711,14 +1711,21 @@ fn build_project_owner_announcement_events(
announcements
.into_iter()
.map(|template| {
if !matches!(template.kind, 30_617 | 30_621) {
if !matches!(template.kind, 30_617 | 30_621 | 47_001) {
anyhow::bail!("unsupported project announcement kind");
}
if !template.tags.iter().any(|tag| {
tag.first().is_some_and(|value| value == "d")
&& tag.get(1).is_some_and(|value| !value.trim().is_empty())
}) {
anyhow::bail!("project announcement is missing its address");
let required_tags: &[&str] = if template.kind == 47_001 {
&["a", "e", "op", "channel"]
} else {
&["d"]
};
for required_tag in required_tags {
if !template.tags.iter().any(|tag| {
tag.first().is_some_and(|value| value == required_tag)
&& tag.get(1).is_some_and(|value| !value.trim().is_empty())
}) {
anyhow::bail!("project event is missing its {required_tag} tag");
}
}
let tags = template
.tags
Expand Down Expand Up @@ -6056,7 +6063,7 @@ mod owner_control_command_tests {
}

#[test]
fn project_owner_control_signs_only_addressable_project_events() {
fn project_owner_control_signs_only_project_events() {
let keys = Keys::generate();
let events = build_project_owner_announcement_events(
vec![
Expand All @@ -6080,6 +6087,29 @@ mod owner_control_command_tests {
assert_eq!(events.len(), 2);
assert!(events.iter().all(|event| event.pubkey == keys.public_key()));
assert!(events.iter().all(|event| event.verify().is_ok()));

let [revision] = build_project_owner_announcement_events(
vec![ProjectOwnerAnnouncementTemplate {
kind: 47_001,
content: String::new(),
created_at: Some(1),
tags: vec![
vec!["a".to_string(), "30621:owner:project".to_string()],
vec!["e".to_string(), "b".repeat(64)],
vec!["op".to_string(), "add-related-channel".to_string()],
vec![
"channel".to_string(),
"11111111-1111-4111-8111-111111111111".to_string(),
],
],
}],
&keys,
)
.expect("valid project revision")
.try_into()
.expect("one revision event");
assert_eq!(revision.pubkey, keys.public_key());
assert!(revision.verify().is_ok());
}

#[test]
Expand Down
3 changes: 3 additions & 0 deletions crates/buzz-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ pub fn parse_write_response(raw: &str, conflict_msg: &str) -> Result<String, Cli
.and_then(serde_json::Value::as_str)
.unwrap_or("");
if !accepted {
if message.starts_with("conflict:") {
return Err(CliError::Conflict(conflict_msg.to_string()));
}
return Err(CliError::Other(format!("relay rejected event: {message}")));
}
if message == "duplicate" || message.starts_with("duplicate:") {
Expand Down
Loading
Loading