From b60b71cbae40f05f5e11e2df8138ac83065b579c Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 25 Sep 2026 23:58:41 -0700 Subject: [PATCH] docs(auth): require caller credentials for stream data requests --- CLAUDE.md | 29 +++++++++++++++++++++++++++++ src/facade.rs | 8 ++++++++ src/subscriptions/security.rs | 7 +++++++ 3 files changed, 44 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4252e77 --- /dev/null +++ b/CLAUDE.md @@ -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. diff --git a/src/facade.rs b/src/facade.rs index 4bd177f..7051284 100644 --- a/src/facade.rs +++ b/src/facade.rs @@ -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)) /// ``` diff --git a/src/subscriptions/security.rs b/src/subscriptions/security.rs index b4f97de..6df84b0 100644 --- a/src/subscriptions/security.rs +++ b/src/subscriptions/security.rs @@ -79,6 +79,13 @@ pub async fn destination(raw: &str, local: bool) -> Result<(url::Url, Vec