Skip to content
Draft
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
29 changes: 29 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Durable Streams constraints

## Authentication: hard boundary

- **User -> DS HTTP -> DS storage actor MUST use the user's Rivet token.**
Actor lookup/creation and data requests must preserve the caller's authority
so existing Rivet authorization applies. Never substitute or fall back to the
DS process token, including when caller credentials are missing or rejected.
- **DS -> subscriber actor wake uses the DS process token.** Delivery is
asynchronous service work authorized by trusted deployed subscription
metadata; it must not depend on retaining a producer's credentials.
- Before injecting the process token, verify that the source stream and resolved
destination match a subscription declaration from the configured namespace's
`/actors/names` metadata. Build the exact actor wake URL on the configured
gateway origin. A matching gateway hostname or actor name alone is insufficient.
Never forward the process token to arbitrary webhooks or through redirects.
- The process token may also authenticate internal service operations such as
metadata discovery and subscription reconciliation. These are distinct from
executing a user's stream request.

This separation prevents the public DS API from borrowing service privileges to
bypass caller permissions, while allowing durable delivery after a producer's
request and credentials are gone.

**Implementation gap:** the HTTP facade currently receives a shared client
configured with the process token, and webhook injection currently checks only
the gateway origin/path. Caller-token forwarding and the metadata relationship
check remain to be implemented; the existing behavior is not an exception to
these constraints.
8 changes: 8 additions & 0 deletions src/facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ struct FacadeState {

/// Returns a router intended to be nested at the application's stream root:
///
/// # Authentication constraint
/// User data requests, including storage actor lookup/creation, MUST use the
/// caller's Rivet token so existing Rivet authorization applies. Never replace
/// missing or rejected caller credentials with the DS process token. The process
/// token belongs to internal service work and authorized subscriber wakes.
/// TODO(auth): the current shared client uses process credentials; replace this
/// data-request path with caller-scoped credentials before exposing it publicly.
///
/// ```ignore
/// app.nest("/v1/stream", durable_streams_router(client, config))
/// ```
Expand Down
7 changes: 7 additions & 0 deletions src/subscriptions/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ pub async fn destination(raw: &str, local: bool) -> Result<(url::Url, Vec<Socket
Ok((url, addresses))
}

/// Deliver an asynchronous subscriber wake using service authority, never a
/// retained producer token. Hard constraint: process-token injection requires
/// matching the source stream and resolved destination to trusted subscription
/// metadata in the configured namespace, plus the exact trusted actor wake URL.
/// Arbitrary webhooks must not receive process credentials.
/// TODO(auth): gateway_token currently checks only origin/path; the delivery
/// caller must additionally enforce the metadata relationship before injection.
pub async fn deliver(
url: &str,
local: bool,
Expand Down
Loading