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
33 changes: 33 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,39 @@ RELAY_URL=ws://localhost:3000
# BUZZ_RATE_LIMIT_AGENT_ELEVATED_MESSAGES_PER_MIN=300
# BUZZ_RATE_LIMIT_AGENT_PLATFORM_MESSAGES_PER_MIN=600

# Relay-verified identity (disabled by default). When enabled, the relay
# requires authenticated requests to present a valid corporate JWT, then binds
# the configured uid claim to the Nostr pubkey proven by NIP-42/NIP-98. The JWT
# may be injected by a trusted proxy or attached by a first-party client; the
# relay treats both as the same header. Clients must forward the configured
# token header on every authenticated HTTP request and session handshake.
#
# Operational notes for the initial implementation:
# - When a trusted proxy injects this header, it MUST overwrite any inbound
# client-supplied value before forwarding to the relay.
# - Revocation and rotation are explicit database lifecycle operations;
# ordinary authentication never silently replaces a key.
# - JWKS outages fail closed for human JWT authentication. Delegated agent
# admission can still work when the owner binding is already present.
# - DISPLAY_CLAIM is private binding metadata. It is never projected publicly
# unless PUBLIC_DISPLAY_CLAIM is separately configured.
# BUZZ_REQUIRE_CORPORATE_IDENTITY=false
# BUZZ_CORPORATE_IDENTITY_JWT_HEADER=x-forwarded-identity-token
# BUZZ_ALLOW_CORPORATE_IDENTITY_DELEGATION=true
# When a request carries both a JWT and a verified NIP-OA owner declaration,
# choose whether the JWT identifies the signer or the owner binding delegates
# access. Defaults to direct; deployments that inject an owner's JWT into agent
# requests can explicitly select delegated.
# BUZZ_CORPORATE_IDENTITY_AUTH_PRECEDENCE=direct
# BUZZ_CORPORATE_IDENTITY_JWKS_URI=https://idp.example/.well-known/jwks.json
# BUZZ_CORPORATE_IDENTITY_ISSUER=https://idp.example
# BUZZ_CORPORATE_IDENTITY_AUDIENCE=buzz-relay
# BUZZ_CORPORATE_IDENTITY_UID_CLAIM=sub
# BUZZ_CORPORATE_IDENTITY_DISPLAY_CLAIM=email
# Optional, public NIP-85 label. Unset by default to keep identity claims private.
# BUZZ_CORPORATE_IDENTITY_PUBLIC_DISPLAY_CLAIM=display_name
# BUZZ_CORPORATE_IDENTITY_NPUB_CLAIM=buzz_npub

# -----------------------------------------------------------------------------
# Git (NIP-34 bare repositories)
# -----------------------------------------------------------------------------
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,27 @@ jobs:
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz
- name: Relay-verified identity lifecycle tests
run: |
docker exec -e PGPASSWORD="${BUZZ_TEST_POSTGRES_PASSWORD}" buzz-postgres \
psql -U buzz -d postgres -v ON_ERROR_STOP=1 \
-c "CREATE DATABASE buzz_identity_tests"
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E '(package(buzz-db) and test(/identity_binding::tests/)) or (package(buzz-relay) and test(/corporate_identity::tests/))' \
--test-threads 1 \
--run-ignored ignored-only
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz_identity_tests
- name: Corporate identity boundary regressions
run: |
cargo nextest run \
--archive-file target/ci/backend-integration-tests.tar.zst \
-E 'package(buzz-relay) and test(/^(corporate_identity::tests::jwt_validation_rejects_missing_and_malformed_audience_claims|api::bridge::tests::(corporate_identity_disables_x_pubkey_bridge_fallback|moderation_reads_require_corporate_identity_after_nip98_proof)|api::media::tests::protected_media_reads_require_corporate_identity_for_get_and_head)$/)' \
--test-threads 1 \
--run-ignored all
env:
DATABASE_URL: postgres://buzz:${{ env.BUZZ_TEST_POSTGRES_PASSWORD }}@localhost:5432/buzz_identity_tests
- name: Workspace profile (kind:9033) gate tests
# Call-site integration for the 9033 authorization gate: open relay
# rosterless/steward transitions and the closed-relay admin/owner rule,
Expand Down
43 changes: 38 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ postcard = { version = "1", default-features = false, features = ["use-std"]
iroh = { version = "1.0.0-rc.0", default-features = false, features = ["tls-ring"] }
serde_json = "1"
serde_yaml = "0.9"
jsonwebtoken = { version = "10.3", default-features = false, features = ["aws_lc_rs"] }
evalexpr = "11"
cron = "0.16"
# Observability
Expand Down
14 changes: 14 additions & 0 deletions crates/buzz-audit/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ pub enum AuditAction {
RateLimitExceeded,
/// A media file was uploaded via the Blossom endpoint.
MediaUploaded,
/// A corporate identity binding was created.
CorporateIdentityBindingCreated,
/// A corporate identity binding attempt conflicted with an active binding.
CorporateIdentityBindingConflict,
/// A corporate identity binding attempt matched a revoked binding.
CorporateIdentityBindingRevokedAttempt,
}

impl AuditAction {
Expand All @@ -45,6 +51,11 @@ impl AuditAction {
Self::AuthFailure => "auth_failure",
Self::RateLimitExceeded => "rate_limit_exceeded",
Self::MediaUploaded => "media_uploaded",
Self::CorporateIdentityBindingCreated => "corporate_identity_binding_created",
Self::CorporateIdentityBindingConflict => "corporate_identity_binding_conflict",
Self::CorporateIdentityBindingRevokedAttempt => {
"corporate_identity_binding_revoked_attempt"
}
}
}

Expand All @@ -60,6 +71,9 @@ impl AuditAction {
Self::AuthFailure,
Self::RateLimitExceeded,
Self::MediaUploaded,
Self::CorporateIdentityBindingCreated,
Self::CorporateIdentityBindingConflict,
Self::CorporateIdentityBindingRevokedAttempt,
];
}

Expand Down
6 changes: 6 additions & 0 deletions crates/buzz-core/src/kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ pub const KIND_LONG_FORM: u32 = 30023;
/// Parameterized replaceable (NIP-33, 30000–39999 range) — keyed by `(pubkey, kind, d_tag)`.
/// Stored globally (channel_id = NULL); user-owned personal data, not channel-scoped.
pub const KIND_USER_STATUS: u32 = 30315;
/// NIP-85: relay-signed trusted assertion about a user pubkey.
///
/// Buzz uses this standard user-subject assertion kind to project an active
/// enterprise identity binding without exposing the binding's stable uid.
/// The relay authors the event and keys it by the subject pubkey in `d`.
pub const KIND_USER_TRUSTED_ASSERTION: u32 = 30382;
/// NIP-78 / NIP-RS: Per-client read state blob for cross-device read position sync.
/// Parameterized replaceable (NIP-33, 30000–39999 range) — keyed by `(pubkey, kind, d_tag)`.
/// Stored globally (channel_id = NULL); user-owned personal data, not channel-scoped.
Expand Down
Loading
Loading