Compare commits

..

No commits in common. "main" and "v0.2.1" have entirely different histories.
main ... v0.2.1

8 changed files with 8 additions and 176 deletions

View File

@ -22,8 +22,8 @@ aura-users authorizes the acting admin by its own assignability rules).
## Versioning
Every release is tagged at the **same version as the st-peter server** it
targets (see `VERSION`). A client tagged `v0.2.x` speaks the wire contract of
st-peter `v0.2.1`+ (client patch releases may lead the server within a minor line when they only expose existing server surface — v0.2.2 adds API-key wrappers). The gRPC wire format is backward-compatible across patch
targets (see `VERSION`). A client tagged `v0.2.1` speaks the wire contract of
st-peter `v0.2.1`. The gRPC wire format is backward-compatible across patch
releases (field numbers and enum integer values are stable), so a client one
patch behind a server generally interoperates — but match versions for new
surface.
@ -51,9 +51,6 @@ The wrapper surface is the same in all three languages:
- `verify_two_factor(two_factor_id, code)` — complete an OTP challenge
- `lookup_user(actor_id, actor_token, identifier)` — resolve a user by
email/phone/handle
- `verify_api_key(key)` — service-credential path: the key's owning service
user + roles (cached); Rust adds `create_api_key` / `list_api_keys` /
`revoke_api_key` management wrappers
- `bearer(...)` helper + the shared `aura_session` cookie name
The **admin surface** (`AdminClient` — Rust first; Go/TS expose the generated
@ -90,17 +87,17 @@ Then tag at `v$(cat VERSION)`.
Rust (`Cargo.toml`):
```toml
st-peter-client = { git = "https://git.awesomike.com/pub/st-peter-client.git", tag = "v0.2.2" }
st-peter-client = { git = "https://git.awesomike.com/pub/st-peter-client.git", tag = "v0.2.1" }
```
Go:
```bash
go get git.awesomike.com/pub/st-peter-client/go@v0.2.2
go get git.awesomike.com/pub/st-peter-client/go@v0.2.1
```
TypeScript (`package.json`):
```json
"@st-peter/client": "git+https://git.awesomike.com/pub/st-peter-client.git#v0.2.2"
"@st-peter/client": "git+https://git.awesomike.com/pub/st-peter-client.git#v0.2.1"
```

View File

@ -1 +1 @@
0.2.3
0.2.1

View File

@ -116,34 +116,6 @@ func (c *AuthClient) VerifyTokenScoped(ctx context.Context, token string, roleSc
return user, nil
}
// VerifyApiKey verifies a service API key, returning the key's OWNING
// SERVICE USER plus that user's roles — consumers build their context
// identically to a session. Cached ~60s per key, like tokens.
func (c *AuthClient) VerifyApiKey(ctx context.Context, apiKey string) (*pb.AuthenticatedUser, error) {
key := cacheKey(apiKey, []string{"#api-key"}) // namespaced away from tokens
c.mu.RLock()
if e, ok := c.cache[key]; ok && time.Since(e.at) < authCacheTTL {
c.mu.RUnlock()
return e.user, nil
}
c.mu.RUnlock()
resp, err := c.svc.VerifyApiKey(ctx, &pb.VerifyApiKeyRequest{ApiKey: apiKey})
if err != nil {
return nil, err
}
if !resp.GetSuccess() || resp.GetAuthenticatedUser() == nil {
return nil, ErrUnauthorized
}
user := resp.GetAuthenticatedUser()
c.mu.Lock()
c.cache[key] = cacheEntry{user: user, at: time.Now()}
c.mu.Unlock()
return user, nil
}
// LoginOutcome is the result of Login / VerifyTwoFactor. Exactly one of the
// three fields is set.
type LoginOutcome struct {

View File

@ -12,10 +12,6 @@ message Date {
service AuthAdminService {
rpc GetUser (GetUserRequest) returns (UserResponse);
rpc GetUsers (GetUsersRequest) returns (UsersResponse);
// System-token-authed minimal lookup: id + display name ONLY (no PII).
// Auth is the shared system token alone (no actor) internal services only,
// so viewing content (e.g. history) can't be used to harvest user info.
rpc GetUsersData (GetUsersDataRequest) returns (GetUsersDataResponse);
rpc GetUsersByUsernames(GetUsersByUsernamesRequest) returns (UsersResponse);
rpc DeleteUser (DeleteUserRequest) returns (OperationResponse);
rpc RestoreUser (RestoreUserRequest) returns (OperationResponse);
@ -310,24 +306,6 @@ message UsersResponse {
repeated User users = 4;
}
// GetUsersData system-token-only, returns the MINIMUM for display: id + a
// resolved display name. Deliberately omits email/phone/PII so even a trusted
// internal caller can't harvest contact info through it.
message GetUsersDataRequest {
string system_token = 1;
repeated string user_ids = 2;
}
message UserData {
string id = 1;
string name = 2;
}
message GetUsersDataResponse {
bool success = 1;
repeated UserData users = 2;
}
message GetAssignableRolesRequest {
string actor_id = 1;
string actor_token = 2;

View File

@ -1,6 +1,6 @@
[package]
name = "st-peter-client"
version = "0.2.3"
version = "0.2.1"
edition = "2021"
description = "Official Rust client for st-peter (aura-users) — authentication over gRPC with a token-verify cache"
repository = "https://git.awesomike.com/pub/st-peter-client"

View File

@ -234,99 +234,6 @@ impl AuthClient {
Ok(interpret_auth_response(resp))
}
/// Verify a service **API key** (`VerifyApiKey` — same response shape as
/// token verification): yields the key's **owning service user** plus that
/// user's roles, so consumers build their `Ctx` identically to a session
/// (the producer seam: a service user holds targeted roles like anyone).
/// Cached ~60s per key, like tokens.
pub async fn verify_api_key(&self, api_key: &str) -> Result<AuthenticatedUser> {
let key = cache_key(api_key, &["#api-key"]); // namespaced away from tokens
let ttl = std::time::Duration::from_secs(AUTH_CACHE_TTL_SECS);
if let Some(u) = {
let cache = self.cache.read().await;
cache
.get(&key)
.and_then(|(u, at)| (at.elapsed() < ttl).then(|| u.clone()))
} {
return Ok(u);
}
let mut client = self.inner.clone();
let resp = client
.verify_api_key(authpb::VerifyApiKeyRequest {
api_key: api_key.to_string(),
})
.await?
.into_inner();
if !resp.success {
return Err(Error::Unauthorized);
}
let user = resp.authenticated_user.ok_or(Error::Unauthorized)?;
self.cache
.write()
.await
.insert(key, (user.clone(), std::time::Instant::now()));
Ok(user)
}
/// Mint an API key for the session's user (`full key returned ONCE`).
pub async fn create_api_key(
&self,
token: &str,
name: &str,
scopes: Vec<String>,
expires_at: i64, // 0 = never
) -> Result<authpb::CreateApiKeyResponse> {
let resp = self
.inner
.clone()
.create_api_key(authpb::CreateApiKeyRequest {
token: token.to_string(),
name: name.to_string(),
scopes,
expires_at,
})
.await?
.into_inner();
if !resp.success {
return Err(Error::Rejected { code: resp.result_code, message: resp.message });
}
Ok(resp)
}
/// List the session user's API keys.
pub async fn list_api_keys(&self, token: &str) -> Result<Vec<authpb::ApiKeyInfo>> {
let resp = self
.inner
.clone()
.list_api_keys(authpb::ListApiKeysRequest { token: token.to_string() })
.await?
.into_inner();
if !resp.success {
return Err(Error::Rejected { code: resp.result_code, message: resp.message });
}
Ok(resp.keys)
}
/// Revoke an API key by id.
pub async fn revoke_api_key(&self, token: &str, key_id: &str) -> Result<()> {
let resp = self
.inner
.clone()
.revoke_api_key(authpb::RevokeApiKeyRequest {
token: token.to_string(),
key_id: key_id.to_string(),
})
.await?
.into_inner();
if !resp.success {
return Err(Error::Rejected { code: resp.result_code, message: resp.message });
}
Ok(())
}
/// Resolve a st-peter user by exact identifier (email / phone / handle).
///
/// Typical use: a service-admin role-grant page — resolve a colleague's
@ -405,28 +312,6 @@ impl AdminClient {
self.inner.clone()
}
/// System-token user lookup — returns ONLY id + display name (no PII).
/// Authorizes on the shared `system_token` alone (no actor), so an internal
/// service can label "who did this" (e.g. content history) without the
/// end-user's credentials and without exposing contact info. The token must
/// be one registered in st-peter's `system-tokens`.
pub async fn get_users_data(
&self,
system_token: &str,
user_ids: Vec<String>,
) -> Result<Vec<adminpb::UserData>> {
let resp = self
.inner
.clone()
.get_users_data(adminpb::GetUsersDataRequest {
system_token: system_token.to_string(),
user_ids,
})
.await?
.into_inner();
Ok(resp.users)
}
/// Assign a role to a user — targeted when `target_id` is set (the
/// multi-tenancy device: e.g. `cms-content-editor` for one organization),
/// optionally time-bound via `expires_at`.

View File

@ -1,6 +1,6 @@
{
"name": "@st-peter/client",
"version": "0.2.2",
"version": "0.2.1",
"description": "Official TypeScript client for st-peter (aura-users) — authentication over gRPC with a token-verify cache",
"repository": "https://git.awesomike.com/pub/st-peter-client",
"license": "MIT OR Apache-2.0",

Binary file not shown.