feat(sdk): users::email_available for anonymous registration pre-check (S1)

`users::find_by_email` requires an authenticated principal (F-S-003,
anti-enumeration), so the natural check-then-create register pattern 502s
for anonymous public scripts. Add `users::email_available(email) -> bool`,
gated like `create` (the registration write path) but without the
anonymous rejection: it leaks only a single boolean — no more than
`create`'s own uniqueness error already does — so self-serve register
scripts can pre-check. Also document find_by_email's principal
requirement in the SDK doc-comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-12 20:43:18 +02:00
parent ae2134e62d
commit 7ffbb8de87
4 changed files with 64 additions and 1 deletions

View File

@@ -516,6 +516,23 @@ impl UsersService for UsersServiceImpl {
Ok(Some(Self::to_app_user(row, roles)))
}
async fn email_available(&self, cx: &SdkCallCx, email: &str) -> Result<bool, UsersError> {
// Gated like `create` (the registration write path) so the probe
// is available exactly where self-serve registration is — and,
// crucially, WITHOUT find_by_email's anonymous-principal rejection.
// It returns only a boolean, so it leaks no more than `create`'s
// own uniqueness error already does (F-S-003: enumeration risk is
// bounded to "exists / doesn't").
self.require(cx.principal.as_ref(), Capability::AppUsersWrite(cx.app_id))
.await?;
let normalized = validate_email(email)?;
Ok(self
.users
.find_by_email(cx.app_id, &normalized)
.await?
.is_none())
}
async fn update(
&self,
cx: &SdkCallCx,