first commit
Polyglot client for st-peter (aura-users) — Rust / Go / TypeScript, one vendored auth proto, versioned in lockstep with the st-peter server (v0.2.0). Mirrors waymaker-client's layout: proto/ + scripts/ + per-language packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
dae39e984f
|
|
@ -0,0 +1,5 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
ts/node_modules/
|
||||||
|
ts/dist/
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Root workspace so the Rust crate in rust/ is discoverable when this repo is
|
||||||
|
# consumed as a Cargo git dependency, and so `cargo` works from the repo root.
|
||||||
|
# The go/ and ts/ clients are independent toolchains and ignored by Cargo.
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = ["rust"]
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
# st-peter-client
|
||||||
|
|
||||||
|
Official client libraries for [st-peter](https://git.awesomike.com/dev/st-peter-lib)
|
||||||
|
(deployed as **aura-users**) — the central authentication service — over gRPC.
|
||||||
|
|
||||||
|
Three clients, one wire contract, **versioned in lockstep with the st-peter
|
||||||
|
server**:
|
||||||
|
|
||||||
|
| Language | Path | Package |
|
||||||
|
|------------|---------|---------|
|
||||||
|
| Rust | `rust/` | `st-peter-client` (crate) |
|
||||||
|
| Go | `go/` | `git.awesomike.com/pub/st-peter-client/go` (module) |
|
||||||
|
| TypeScript | `ts/` | `@st-peter/client` (npm) |
|
||||||
|
|
||||||
|
The `.proto` file in `proto/` is a **vendored copy**; the st-peter server repo
|
||||||
|
is the source of truth. `scripts/sync-protos.sh` refreshes it and keeps
|
||||||
|
`VERSION` aligned with the server. Only the **auth** surface is vendored —
|
||||||
|
services authenticate users; they do not administer st-peter.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Every release is tagged at the **same version as the st-peter server** it
|
||||||
|
targets (see `VERSION`). A client tagged `v0.2.0` speaks the wire contract of
|
||||||
|
st-peter `v0.2.0`. 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.
|
||||||
|
|
||||||
|
## Design: authentication central, authorization local
|
||||||
|
|
||||||
|
st-peter answers *who is this token?* — every client returns the verified
|
||||||
|
identity plus the user's **platform** roles, with a ~60s token-verify cache.
|
||||||
|
What that identity may do inside a consuming service (media roles, CMS
|
||||||
|
roles, …) is that service's own concern: keep a local roles table keyed by
|
||||||
|
the st-peter `user_id` **by value** (no cross-DB FK) and map permissions
|
||||||
|
there. These clients deliberately ship no session/permission types.
|
||||||
|
|
||||||
|
The wrapper surface is the same in all three languages:
|
||||||
|
|
||||||
|
- `connect(target)` — lazy dial; failures surface on the first call
|
||||||
|
- `verify_token(token)` — verified user + platform roles, cached ~60s
|
||||||
|
- `login(identifier, password)` → `Authenticated | TwoFactor | Failed`
|
||||||
|
- `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 (for local role-grant flows)
|
||||||
|
- `bearer(...)` helper + the shared `aura_session` cookie name
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
proto/ vendored st-peter-auth.proto (source of truth: st-peter repo)
|
||||||
|
rust/ cargo package; stubs compiled at build time from ../proto
|
||||||
|
go/ go module; committed stubs in genpb/ (scripts/gen-go.sh)
|
||||||
|
ts/ npm package; committed stubs in src/genpb (scripts/gen-ts.sh)
|
||||||
|
scripts/ sync-protos.sh, gen-go.sh, gen-ts.sh
|
||||||
|
VERSION the st-peter server version this client targets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regenerating after a proto change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ST_PETER_REPO=/path/to/st-peter-lib ./scripts/sync-protos.sh
|
||||||
|
./scripts/gen-go.sh # needs protoc-gen-go{,-grpc}
|
||||||
|
(cd ts && npm install) && ./scripts/gen-ts.sh
|
||||||
|
cargo check # rust stubs regenerate via build.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
Then tag at `v$(cat VERSION)`.
|
||||||
|
|
||||||
|
## Consuming
|
||||||
|
|
||||||
|
Rust (`Cargo.toml`):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
st-peter-client = { git = "https://git.awesomike.com/pub/st-peter-client.git", tag = "v0.2.0" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Go:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.awesomike.com/pub/st-peter-client/go@v0.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
TypeScript (`package.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"@st-peter/client": "git+https://git.awesomike.com/pub/st-peter-client.git#v0.2.0"
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# st-peter-client (Go)
|
||||||
|
|
||||||
|
Official Go client for st-peter (aura-users). Generated stubs are committed
|
||||||
|
under `genpb/` (regenerate with `../scripts/gen-go.sh`); the `stpeter.AuthClient`
|
||||||
|
wrapper (token-verify cache, login/2FA/lookup) is layered on top.
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth, err := stpeter.Connect("127.0.0.1:9091")
|
||||||
|
user, err := auth.VerifyToken(ctx, token) // cached ~60s
|
||||||
|
```
|
||||||
|
|
||||||
|
See the repo root README for versioning and the
|
||||||
|
authentication-central / authorization-local design.
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
// Package stpeter is the official Go client for st-peter (aura-users) —
|
||||||
|
// the central authentication service.
|
||||||
|
//
|
||||||
|
// Design: authentication central, authorization local. st-peter answers
|
||||||
|
// *who is this token?* — it returns the verified identity plus the user's
|
||||||
|
// platform roles. What that identity may do inside a consuming service is
|
||||||
|
// that service's own concern: keep a local roles table keyed by the
|
||||||
|
// st-peter user id by value (no cross-DB FK) and map permissions there.
|
||||||
|
// This package deliberately ships no session/permission types.
|
||||||
|
package stpeter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pb "git.awesomike.com/pub/st-peter-client/go/genpb/auth"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CookieName is the session cookie issued by aura-users on login. Shared
|
||||||
|
// convention across the aura services so a session works on any of them.
|
||||||
|
const CookieName = "aura_session"
|
||||||
|
|
||||||
|
// authCacheTTL bounds how long a verified token is served from cache. A
|
||||||
|
// revoked token stays "valid" for up to this long — logout flows must not
|
||||||
|
// rely on the cache.
|
||||||
|
const authCacheTTL = 60 * time.Second
|
||||||
|
|
||||||
|
// ErrUnauthorized is returned when a token fails verification (or the
|
||||||
|
// response carries no user).
|
||||||
|
var ErrUnauthorized = errors.New("stpeter: unauthorized")
|
||||||
|
|
||||||
|
type cacheEntry struct {
|
||||||
|
user *pb.AuthenticatedUser
|
||||||
|
at time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthClient is a thin client to aura-users' AuthService with a small
|
||||||
|
// token-verify cache. It is safe for concurrent use; the underlying gRPC
|
||||||
|
// channel is reference-counted.
|
||||||
|
type AuthClient struct {
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
svc pb.AuthServiceClient
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
cache map[uint64]cacheEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect dials aura-users and returns an AuthClient. The target is a gRPC
|
||||||
|
// target (e.g. "127.0.0.1:9091" — no scheme). The dial is lazy: Connect
|
||||||
|
// returns immediately and failures surface on the first call.
|
||||||
|
func Connect(target string, opts ...grpc.DialOption) (*AuthClient, error) {
|
||||||
|
defaults := []grpc.DialOption{
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
}
|
||||||
|
opts = append(defaults, opts...)
|
||||||
|
conn, err := grpc.NewClient(target, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stpeter: dial %q: %w", target, err)
|
||||||
|
}
|
||||||
|
return &AuthClient{
|
||||||
|
conn: conn,
|
||||||
|
svc: pb.NewAuthServiceClient(conn),
|
||||||
|
cache: make(map[uint64]cacheEntry),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close tears down the underlying gRPC channel.
|
||||||
|
func (c *AuthClient) Close() error { return c.conn.Close() }
|
||||||
|
|
||||||
|
// VerifyToken verifies a session token, returning the authenticated user
|
||||||
|
// (+ platform roles). Verifications are cached ~60s to avoid a gRPC
|
||||||
|
// round-trip per request. Returns ErrUnauthorized on a bad token.
|
||||||
|
func (c *AuthClient) VerifyToken(ctx context.Context, token string) (*pb.AuthenticatedUser, error) {
|
||||||
|
key := tokenHash(token)
|
||||||
|
|
||||||
|
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.VerifyToken(ctx, &pb.VerifyTokenRequest{
|
||||||
|
Token: token,
|
||||||
|
IncludeUserRoles: true, // platform roles; service roles stay local
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Gate on Success first, then the user. The verified user is carried on
|
||||||
|
// AuthenticationResponse.authenticated_user.
|
||||||
|
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 {
|
||||||
|
// Authenticated carries the session (token + user + platform roles) on
|
||||||
|
// success. A login bridge sets the CookieName cookie from its token.
|
||||||
|
Authenticated *pb.AuthenticatedUser
|
||||||
|
// TwoFactorID is non-empty when aura-users issued a one-time passcode
|
||||||
|
// challenge; collect the code and call VerifyTwoFactor with it.
|
||||||
|
TwoFactorID string
|
||||||
|
// FailureMessage is non-empty when the login failed (bad credentials,
|
||||||
|
// locked out, …) — a display message.
|
||||||
|
FailureMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// interpretAuthResponse maps an AuthenticationResponse to a LoginOutcome
|
||||||
|
// (shared by Login and VerifyTwoFactor): 2FA-required first, then success,
|
||||||
|
// else fail.
|
||||||
|
func interpretAuthResponse(resp *pb.AuthenticationResponse) LoginOutcome {
|
||||||
|
if resp.GetPassCodeRequired() {
|
||||||
|
return LoginOutcome{TwoFactorID: resp.GetTwoFactorId()}
|
||||||
|
}
|
||||||
|
if resp.GetSuccess() && resp.GetAuthenticatedUser() != nil {
|
||||||
|
return LoginOutcome{Authenticated: resp.GetAuthenticatedUser()}
|
||||||
|
}
|
||||||
|
msg := resp.GetMessage()
|
||||||
|
if msg == "" {
|
||||||
|
msg = "Invalid email or password."
|
||||||
|
}
|
||||||
|
return LoginOutcome{FailureMessage: msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login authenticates by identifier (email/phone) + password.
|
||||||
|
func (c *AuthClient) Login(ctx context.Context, identifier, password string) (LoginOutcome, error) {
|
||||||
|
resp, err := c.svc.Login(ctx, &pb.LoginRequest{
|
||||||
|
UserIdentifier: identifier,
|
||||||
|
Password: password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return LoginOutcome{}, err
|
||||||
|
}
|
||||||
|
return interpretAuthResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyTwoFactor completes a two-factor challenge with the OTP code,
|
||||||
|
// returning the authenticated session on success.
|
||||||
|
func (c *AuthClient) VerifyTwoFactor(ctx context.Context, twoFactorID, code string) (LoginOutcome, error) {
|
||||||
|
resp, err := c.svc.VerifyTwoFactor(ctx, &pb.VerifyTwoFactorRequest{
|
||||||
|
TwoFactorId: twoFactorID,
|
||||||
|
Code: code,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return LoginOutcome{}, err
|
||||||
|
}
|
||||||
|
return interpretAuthResponse(resp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupUser resolves a st-peter user by exact identifier (email / phone /
|
||||||
|
// handle). LookupUser requires an authenticated actor — pass the acting
|
||||||
|
// user's own user id + session token. Returns (nil, nil) when not found.
|
||||||
|
func (c *AuthClient) LookupUser(ctx context.Context, actorUserID, actorToken, identifier string) (*pb.User, error) {
|
||||||
|
resp, err := c.svc.LookupUser(ctx, &pb.LookupUserRequest{
|
||||||
|
UserId: actorUserID,
|
||||||
|
UserToken: actorToken,
|
||||||
|
Identifier: identifier,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !resp.GetSuccess() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return resp.GetUser(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearer extracts a `Bearer <token>` from an Authorization header value.
|
||||||
|
// The shared convention across aura services is: Bearer header first, then
|
||||||
|
// the CookieName session cookie.
|
||||||
|
func Bearer(authorization string) (string, bool) {
|
||||||
|
tok, ok := strings.CutPrefix(authorization, "Bearer ")
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(tok), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenHash is a fast (non-cryptographic) hash of the token, used only as
|
||||||
|
// the cache key.
|
||||||
|
func tokenHash(token string) uint64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
_, _ = h.Write([]byte(token))
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,15 @@
|
||||||
|
module git.awesomike.com/pub/st-peter-client/go
|
||||||
|
|
||||||
|
go 1.26.2
|
||||||
|
|
||||||
|
require (
|
||||||
|
google.golang.org/grpc v1.81.1
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||||
|
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||||
|
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||||
|
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
|
@ -0,0 +1,633 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
package st_peter.auth;
|
||||||
|
option go_package = "nandie.com/pkg/;auth_service";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
service AuthService {
|
||||||
|
rpc RegisterUser (RegisterUserRequest) returns (RegisterUserResponse);
|
||||||
|
rpc VerifyRegisterUser (VerifyRegisterUserRequest) returns (AuthenticationResponse);
|
||||||
|
rpc Login (LoginRequest) returns (AuthenticationResponse);
|
||||||
|
rpc Logout (LogoutRequest) returns (LogoutResponse);
|
||||||
|
rpc VerifyToken (VerifyTokenRequest) returns (AuthenticationResponse);
|
||||||
|
rpc VerifyAuthClaimToken (VerifyAuthClaimTokenRequest) returns (AuthenticationResponse);
|
||||||
|
rpc InitiateTwoFactor (InitiateTwoFactorRequest) returns (InitiateTwoFactorResponse);
|
||||||
|
rpc VerifyTwoFactor (VerifyTwoFactorRequest) returns (AuthenticationResponse);
|
||||||
|
rpc InitiatePasswordReset (InitiatePasswordResetRequest) returns (PasswordResetTokenResponse);
|
||||||
|
rpc VerifyPasswordResetToken (VerifyPasswordResetTokenRequest) returns (PasswordResetTokenResponse);
|
||||||
|
rpc ResetPassword (ResetPasswordRequest) returns (AuthenticationResponse);
|
||||||
|
rpc ResendVerificationCode (ResendVerificationRequest) returns (ResendVerificationResponse);
|
||||||
|
rpc UpdateUserInfo (UpdateUserInfoRequest) returns (UpdateUserInfoResponse);
|
||||||
|
rpc ChangeIdentityField (ChangeIdentityFieldRequest) returns (ChangeIdentityFieldResponse);
|
||||||
|
rpc VerifyIdentityField (VerifyIdentityFieldRequest) returns (VerifyIdentityFieldResponse);
|
||||||
|
rpc ResendIdentifierChangeCode(ResendIdentityFieldRequest) returns (ChangeIdentityFieldResponse);
|
||||||
|
rpc UpdateUserPreference (UpdateUserPreferenceRequest) returns (OperationResponse);
|
||||||
|
rpc GetUserPreferenceByCode (GetUserPreferenceByCodeRequest) returns (GetUserPreferenceByCodeResponse);
|
||||||
|
rpc GetUserSessions(GetUserSessionsRequest) returns (GetUserSessionsResponse);
|
||||||
|
rpc ClearUserSessions(ClearUserSessionsRequest) returns (ClearUserSessionsResponse);
|
||||||
|
|
||||||
|
// Social login - Mobile flow (validates ID token from native SDK)
|
||||||
|
rpc SocialLogin(SocialLoginRequest) returns (SocialLoginResponse);
|
||||||
|
// Social login - Web OAuth flow
|
||||||
|
rpc InitiateOAuth(InitiateOAuthRequest) returns (InitiateOAuthResponse);
|
||||||
|
rpc CompleteOAuth(OAuthCallbackRequest) returns (SocialLoginResponse);
|
||||||
|
// Account linking
|
||||||
|
rpc LinkSocialAccount(LinkSocialAccountRequest) returns (OperationResponse);
|
||||||
|
rpc UnlinkSocialAccount(UnlinkSocialAccountRequest) returns (OperationResponse);
|
||||||
|
rpc GetLinkedAccounts(GetLinkedAccountsRequest) returns (GetLinkedAccountsResponse);
|
||||||
|
|
||||||
|
// API Keys
|
||||||
|
rpc CreateApiKey(CreateApiKeyRequest) returns (CreateApiKeyResponse);
|
||||||
|
rpc ListApiKeys(ListApiKeysRequest) returns (ListApiKeysResponse);
|
||||||
|
rpc RevokeApiKey(RevokeApiKeyRequest) returns (OperationResponse);
|
||||||
|
rpc VerifyApiKey(VerifyApiKeyRequest) returns (AuthenticationResponse);
|
||||||
|
|
||||||
|
// Password policy (public, no auth required)
|
||||||
|
rpc GetPasswordPolicy(GetPasswordPolicyRequest) returns (GetPasswordPolicyResponse);
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
rpc GetMetrics(GetMetricsRequest) returns (GetMetricsResponse);
|
||||||
|
|
||||||
|
// User lookup by identifier (email, phone, or handle) — any authenticated user
|
||||||
|
rpc LookupUser(LookupUserRequest) returns (LookupUserResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message Date {
|
||||||
|
int32 year = 1;
|
||||||
|
uint32 month = 2;
|
||||||
|
uint32 day = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ResultCode {
|
||||||
|
RESULT_CODE_SUCCESS = 0;
|
||||||
|
RESULT_CODE_NOT_FOUND = 1;
|
||||||
|
RESULT_CODE_INTERNAL_SERVER_ERROR = 2;
|
||||||
|
RESULT_CODE_BAD_REQUEST = 3;
|
||||||
|
RESULT_CODE_NOT_AUTHORIZED = 4;
|
||||||
|
RESULT_CODE_FORBIDDEN = 5;
|
||||||
|
RESULT_CODE_VALIDATION_ERRORS = 6;
|
||||||
|
RESULT_CODE_PASSCODE_REQUIRED = 7;
|
||||||
|
RESULT_CODE_TOO_MANY_REQUESTS = 9;
|
||||||
|
RESULT_CODE_INVALID_CREDENTIALS = 8;
|
||||||
|
RESULT_CODE_INACTIVE_USER = 10;
|
||||||
|
RESULT_CODE_IDENTITY_IN_USE = 11;
|
||||||
|
RESULT_CODE_NEXT_STEP = 12;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChannelType {
|
||||||
|
CHANNEL_TYPE_UNSPECIFIED = 0;
|
||||||
|
CHANNEL_TYPE_EMAIL = 1;
|
||||||
|
CHANNEL_TYPE_SMS = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VerificationCodeType {
|
||||||
|
VERIFICATION_CODE_TYPE_UNSPECIFIED = 0;
|
||||||
|
VERIFICATION_CODE_TYPE_REGISTER = 1;
|
||||||
|
VERIFICATION_CODE_TYPE_PASSWORD_RESET = 2;
|
||||||
|
VERIFICATION_CODE_TYPE_OTP_LOGIN = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message User {
|
||||||
|
string id = 1;
|
||||||
|
string email = 2;
|
||||||
|
string phone = 3;
|
||||||
|
string first_names = 4;
|
||||||
|
string last_name = 5;
|
||||||
|
string profile_picture_url = 6;
|
||||||
|
optional string handle = 7;
|
||||||
|
google.protobuf.Timestamp created_at = 10;
|
||||||
|
google.protobuf.Timestamp updated_at = 11;
|
||||||
|
google.protobuf.Timestamp deleted_at = 12;
|
||||||
|
google.protobuf.Timestamp last_login = 13;
|
||||||
|
bool is_active = 20;
|
||||||
|
bool is_email_verified = 21;
|
||||||
|
bool is_phone_verified = 22;
|
||||||
|
Date date_of_birth = 23;
|
||||||
|
int64 version = 24;
|
||||||
|
repeated SocialAccount social_accounts = 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserPreference {
|
||||||
|
string id = 1;
|
||||||
|
string user_id = 2;
|
||||||
|
string preference_code = 3;
|
||||||
|
string preference_value_type = 4;
|
||||||
|
string preference_value = 5;
|
||||||
|
google.protobuf.Timestamp created_at = 10;
|
||||||
|
google.protobuf.Timestamp updated_at = 11;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
message Role {
|
||||||
|
string id = 1;
|
||||||
|
string code = 2;
|
||||||
|
string description = 3;
|
||||||
|
google.protobuf.Timestamp created_at = 4;
|
||||||
|
google.protobuf.Timestamp updated_at = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SocialAccount {
|
||||||
|
string provider = 1;
|
||||||
|
string provider_user_id = 2;
|
||||||
|
string access_token = 3;
|
||||||
|
google.protobuf.Timestamp expires_at = 4;
|
||||||
|
string email = 5;
|
||||||
|
string name = 6;
|
||||||
|
string profile_picture_url = 7;
|
||||||
|
bool email_verified = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuth Provider enumeration
|
||||||
|
enum OAuthProvider {
|
||||||
|
OAUTH_PROVIDER_UNSPECIFIED = 0;
|
||||||
|
OAUTH_PROVIDER_GOOGLE = 1;
|
||||||
|
OAUTH_PROVIDER_APPLE = 2;
|
||||||
|
OAUTH_PROVIDER_FACEBOOK = 3;
|
||||||
|
OAUTH_PROVIDER_MICROSOFT = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mobile flow - validate ID token from native SDK
|
||||||
|
message SocialLoginRequest {
|
||||||
|
OAuthProvider provider = 1;
|
||||||
|
string id_token = 2; // JWT from Google/Apple SDK
|
||||||
|
string access_token = 3; // For Facebook
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
string nonce = 5; // For Apple Sign-In security
|
||||||
|
}
|
||||||
|
|
||||||
|
message SocialLoginResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
AuthenticatedUser authenticated_user = 4;
|
||||||
|
bool is_new_user = 5;
|
||||||
|
bool was_auto_linked = 6;
|
||||||
|
repeated ValidationError validation_errors = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web flow - initiate OAuth redirect
|
||||||
|
message InitiateOAuthRequest {
|
||||||
|
OAuthProvider provider = 1;
|
||||||
|
string redirect_uri = 2;
|
||||||
|
string state = 3; // Client-provided state for additional verification
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiateOAuthResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string authorization_url = 4;
|
||||||
|
string state = 5; // Server-generated state token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web flow - handle callback
|
||||||
|
message OAuthCallbackRequest {
|
||||||
|
OAuthProvider provider = 1;
|
||||||
|
string code = 2;
|
||||||
|
string state = 3;
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Account linking
|
||||||
|
message LinkSocialAccountRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
OAuthProvider provider = 3;
|
||||||
|
string id_token = 4;
|
||||||
|
string access_token = 5;
|
||||||
|
string nonce = 6; // For Apple Sign-In
|
||||||
|
}
|
||||||
|
|
||||||
|
message UnlinkSocialAccountRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
OAuthProvider provider = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLinkedAccountsRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetLinkedAccountsResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
repeated SocialAccount accounts = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterUserRequest {
|
||||||
|
string user_identifier = 1;
|
||||||
|
string password = 2;
|
||||||
|
string first_names = 4;
|
||||||
|
string last_name = 5;
|
||||||
|
string app_hash = 6;
|
||||||
|
DeviceInfo device_info = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RegisterUserResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string registration_id = 4;
|
||||||
|
repeated ValidationError validation_errors = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message VerifyRegisterUserRequest {
|
||||||
|
string user_identifier = 1; // Can be either email or phone
|
||||||
|
string registration_id = 2;
|
||||||
|
string token = 3; //Token has a redirect to encoded in it.
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
User user = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeviceInfo {
|
||||||
|
string application_name = 1; // e.g., 'web', 'mobile'
|
||||||
|
string application_version = 2; //e.g., '1.0.0'
|
||||||
|
string device_name = 3; // e.g., 'iPhone X', 'Pixel 2'
|
||||||
|
string device_type = 4; // -- e.g., 'desktop', 'mobile'
|
||||||
|
string device_os = 5; // e. g., 'iOS', 'Android', 'Windows'
|
||||||
|
string device_os_version = 6; // e.g., '10', '11.4.1'
|
||||||
|
string device_id = 7; // e.g., 'imei:1234567890', 'serial:1234567890'
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string user_identifier = 1; // Can be either email or phone
|
||||||
|
string password = 2;
|
||||||
|
string app_hash = 3;
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthenticationResponse {
|
||||||
|
bool success = 1;
|
||||||
|
bool pass_code_required = 2;
|
||||||
|
string two_factor_id = 3;
|
||||||
|
ResultCode result_code = 8;
|
||||||
|
string message = 9;
|
||||||
|
AuthenticatedUser authenticated_user = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AuthenticatedUser {
|
||||||
|
User user = 1;
|
||||||
|
string token = 2;
|
||||||
|
string session_id = 3;
|
||||||
|
google.protobuf.Timestamp expires_at = 4;
|
||||||
|
repeated UserPreference user_preferences = 5;
|
||||||
|
repeated AssignedUserRole user_roles = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AssignedUserRole {
|
||||||
|
string id = 1;
|
||||||
|
string user_id = 2;
|
||||||
|
string role_id = 3;
|
||||||
|
string role_name = 4;
|
||||||
|
string scope_code = 5;
|
||||||
|
string target_id = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyTokenRequest {
|
||||||
|
string token = 1;
|
||||||
|
bool include_user_roles = 2;
|
||||||
|
repeated string role_scopes = 3;
|
||||||
|
repeated string role_names = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyAuthClaimTokenRequest {
|
||||||
|
string claim = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiateTwoFactorResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string two_factor_id = 4;
|
||||||
|
ChannelType channel = 5;
|
||||||
|
string user_id = 7;
|
||||||
|
repeated ValidationError validation_errors = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyTwoFactorResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
repeated ValidationError validation_errors = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiateTwoFactorRequest {
|
||||||
|
optional string user_id = 1;
|
||||||
|
optional string user_identifier = 5; // Can be either email or phone
|
||||||
|
ChannelType channel = 2;
|
||||||
|
string app_hash = 3;
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyTwoFactorRequest {
|
||||||
|
string two_factor_id = 1;
|
||||||
|
string code = 2;
|
||||||
|
DeviceInfo device_info = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitiatePasswordResetRequest {
|
||||||
|
string user_identifier = 1; // Can be either email or phone
|
||||||
|
string app_hash = 2;
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
string new_password = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OperationResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyPasswordResetTokenRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string password_reset_id = 2;
|
||||||
|
string passcode = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message PasswordResetTokenResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string password_reset_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResetPasswordRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string password_reset_id = 2;
|
||||||
|
string passcode = 3;
|
||||||
|
DeviceInfo device_info = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum IdentityField {
|
||||||
|
IDENTITY_FIELD_UNSPECIFIED = 0;
|
||||||
|
IDENTITY_FIELD_EMAIL = 1;
|
||||||
|
IDENTITY_FIELD_phone = 2;
|
||||||
|
IDENTITY_FIELD_PASSWORD = 3;
|
||||||
|
IDENTITY_FIELD_HANDLE = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChangeIdentityFieldRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string user_token = 2;
|
||||||
|
ChannelType channel = 3;
|
||||||
|
string new_value = 4;
|
||||||
|
IdentityField field_type = 5;
|
||||||
|
string app_hash = 6;
|
||||||
|
DeviceInfo device_info = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChangeIdentityFieldResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
|
||||||
|
string challenge_id = 4;
|
||||||
|
int32 passcode_size = 5;
|
||||||
|
string user_identifier = 6;
|
||||||
|
ChannelType channel = 7;
|
||||||
|
repeated ValidationError validation_errors = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyIdentityFieldRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string user_token = 2;
|
||||||
|
string challenge_id = 4;
|
||||||
|
string verification_code = 5;
|
||||||
|
|
||||||
|
string app_hash = 6;
|
||||||
|
DeviceInfo device_info = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResendIdentityFieldRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string user_token = 2;
|
||||||
|
string challenge_id = 4;
|
||||||
|
|
||||||
|
string app_hash = 6;
|
||||||
|
DeviceInfo device_info = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
message VerifyIdentityFieldResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
|
||||||
|
bool requires_verification = 8;
|
||||||
|
string challenge_id = 4;
|
||||||
|
int32 passcode_size = 5;
|
||||||
|
string user_identifier = 6;
|
||||||
|
ChannelType channel = 7;
|
||||||
|
|
||||||
|
repeated ValidationError validation_errors = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserInfoRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string user_token = 2;
|
||||||
|
optional string first_names = 3;
|
||||||
|
optional string last_name = 4;
|
||||||
|
optional string profile_picture_id = 6;
|
||||||
|
Date date_of_birth = 5;
|
||||||
|
optional string handle = 7; // Optional unique handle (e.g., @username)
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserInfoResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
|
||||||
|
User user = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LogoutRequest {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LogoutResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ValidationError {
|
||||||
|
string field = 1;
|
||||||
|
string message = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Scope {
|
||||||
|
string code = 1;
|
||||||
|
string description = 2;
|
||||||
|
optional string parent_code = 3;
|
||||||
|
bool is_active = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UpdateUserPreferenceRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
string preference_key = 3;
|
||||||
|
string preference_value = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserPreferenceByCodeRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
string preference_code = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserPreferenceByCodeResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string value = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResendVerificationRequest {
|
||||||
|
string user_identifier = 1;
|
||||||
|
VerificationCodeType verification_code_type = 2;
|
||||||
|
string app_hash = 3;
|
||||||
|
string verification_code_id = 4;
|
||||||
|
DeviceInfo device_info = 19;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ResendVerificationResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string verification_code_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserSession {
|
||||||
|
string id = 1;
|
||||||
|
string user_id = 2;
|
||||||
|
DeviceInfo device_info = 3;
|
||||||
|
google.protobuf.Timestamp created_at = 4;
|
||||||
|
google.protobuf.Timestamp expires_at = 5;
|
||||||
|
google.protobuf.Timestamp last_activity = 6;
|
||||||
|
bool is_active = 7;
|
||||||
|
string ip_address = 8;
|
||||||
|
string user_agent = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserSessionsRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
int32 page = 3;
|
||||||
|
int32 size = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetUserSessionsResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
repeated UserSession sessions = 4;
|
||||||
|
int32 total = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClearUserSessionsRequest {
|
||||||
|
string actor_id = 1;
|
||||||
|
string actor_token = 2;
|
||||||
|
repeated string session_ids = 3; // If empty, clears all sessions except current
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClearUserSessionsResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
int32 cleared_count = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
message GetMetricsRequest {
|
||||||
|
string bearer_token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetMetricsResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string metrics = 4; // Prometheus text format
|
||||||
|
}
|
||||||
|
|
||||||
|
// API Key messages
|
||||||
|
message CreateApiKeyRequest {
|
||||||
|
string token = 1; // existing session token for auth
|
||||||
|
string name = 2;
|
||||||
|
repeated string scopes = 3;
|
||||||
|
int64 expires_at = 4; // 0 = never
|
||||||
|
}
|
||||||
|
|
||||||
|
message CreateApiKeyResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string api_key = 4; // full key, shown ONCE
|
||||||
|
string key_id = 5;
|
||||||
|
string key_prefix = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListApiKeysRequest {
|
||||||
|
string token = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListApiKeysResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
repeated ApiKeyInfo keys = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ApiKeyInfo {
|
||||||
|
string id = 1;
|
||||||
|
string name = 2;
|
||||||
|
string key_prefix = 3;
|
||||||
|
repeated string scopes = 4;
|
||||||
|
int64 last_used_at = 5;
|
||||||
|
int64 expires_at = 6;
|
||||||
|
int64 created_at = 7;
|
||||||
|
bool is_active = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RevokeApiKeyRequest {
|
||||||
|
string token = 1;
|
||||||
|
string key_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VerifyApiKeyRequest {
|
||||||
|
string api_key = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password policy
|
||||||
|
message GetPasswordPolicyRequest {}
|
||||||
|
|
||||||
|
message GetPasswordPolicyResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
uint32 min_length = 4;
|
||||||
|
bool requires_uppercase = 5;
|
||||||
|
bool requires_special_character = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup user by exact identifier (email, phone number, or handle)
|
||||||
|
message LookupUserRequest {
|
||||||
|
string user_id = 1;
|
||||||
|
string user_token = 2;
|
||||||
|
string identifier = 3; // email, phone number, or handle
|
||||||
|
}
|
||||||
|
|
||||||
|
message LookupUserResponse {
|
||||||
|
bool success = 1;
|
||||||
|
ResultCode result_code = 2;
|
||||||
|
string message = 3;
|
||||||
|
optional User user = 4;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "st-peter-client"
|
||||||
|
version = "0.2.0"
|
||||||
|
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"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "st_peter_client"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Async runtime. `sync` for the token-verify cache RwLock; nothing else.
|
||||||
|
tokio = { version = "1", default-features = false, features = ["sync", "time"] }
|
||||||
|
thiserror = "2"
|
||||||
|
|
||||||
|
# gRPC client. tls-ring lets the client talk to a TLS-protected aura-users
|
||||||
|
# without dragging in any server-side TLS termination code.
|
||||||
|
tonic = { version = "0.14", features = ["codegen", "tls-ring"] }
|
||||||
|
tonic-prost = "0.14"
|
||||||
|
prost = "0.14"
|
||||||
|
prost-types = "0.14"
|
||||||
|
|
||||||
|
# `bearer()` header extraction (the same `http` major tonic uses).
|
||||||
|
http = "1"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
# Compiles the vendored proto in ../proto into client stubs at build time.
|
||||||
|
tonic-prost-build = "0.14"
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# st-peter-client (Rust)
|
||||||
|
|
||||||
|
Official Rust client for st-peter (aura-users). Stubs are generated at build
|
||||||
|
time from `../proto/st-peter-auth.proto`; the ergonomic [`AuthClient`] wrapper
|
||||||
|
(token-verify cache, login/2FA/lookup) is layered on top, with the raw wire
|
||||||
|
surface available under `st_peter_client::authpb`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let auth = st_peter_client::AuthClient::connect("http://127.0.0.1:9091").await?;
|
||||||
|
let user = auth.verify_token(&token).await?; // cached ~60s
|
||||||
|
```
|
||||||
|
|
||||||
|
See the repo root README for versioning and the
|
||||||
|
authentication-central / authorization-local design.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Compile the vendored proto (../proto, synced from the st-peter server repo)
|
||||||
|
// into client stubs. Client-only: no server traits are generated.
|
||||||
|
fn main() {
|
||||||
|
let protos = ["../proto/st-peter-auth.proto"];
|
||||||
|
tonic_prost_build::configure()
|
||||||
|
.build_server(false)
|
||||||
|
.compile_protos(&protos, &["../proto"])
|
||||||
|
.expect("failed to compile st-peter protos");
|
||||||
|
for p in protos {
|
||||||
|
println!("cargo:rerun-if-changed={p}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,261 @@
|
||||||
|
//! Official Rust client for st-peter (aura-users) — the central
|
||||||
|
//! authentication service.
|
||||||
|
//!
|
||||||
|
//! The proto stubs are generated at build time from the vendored
|
||||||
|
//! `.proto` file in this repo's `proto/` directory (the st-peter
|
||||||
|
//! server repo is the source of truth — see `scripts/sync-protos.sh`).
|
||||||
|
//! The raw wire surface lives under [`authpb`]; [`AuthClient`] is the
|
||||||
|
//! ergonomic wrapper layered on top.
|
||||||
|
//!
|
||||||
|
//! ## Design: authentication central, authorization local
|
||||||
|
//!
|
||||||
|
//! st-peter answers *who is this token?* — it returns the verified
|
||||||
|
//! identity plus the user's **platform** roles. What that identity may
|
||||||
|
//! do inside a consuming service (media roles, CMS roles, …) is that
|
||||||
|
//! service's own concern: keep a local roles table keyed by the
|
||||||
|
//! st-peter `user_id` **by value** (no cross-DB FK) and map permissions
|
||||||
|
//! there. This crate deliberately ships no session/permission types.
|
||||||
|
//!
|
||||||
|
//! ## Usage
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! let auth = st_peter_client::AuthClient::connect("http://127.0.0.1:9091").await?;
|
||||||
|
//!
|
||||||
|
//! // per-request: verify a session token (cached ~60s)
|
||||||
|
//! let user = auth.verify_token(&token).await?;
|
||||||
|
//!
|
||||||
|
//! // login bridge
|
||||||
|
//! match auth.login(&identifier, &password).await? {
|
||||||
|
//! LoginOutcome::Authenticated(user) => { /* set session cookie from user.token */ }
|
||||||
|
//! LoginOutcome::TwoFactor { two_factor_id } => { /* collect OTP, verify_two_factor */ }
|
||||||
|
//! LoginOutcome::Failed(msg) => { /* show msg */ }
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use tonic::transport::Channel;
|
||||||
|
|
||||||
|
/// Auth proto stubs (`AuthService` — the external client surface).
|
||||||
|
/// Generated from `proto/st-peter-auth.proto` (package `st_peter.auth`).
|
||||||
|
pub mod authpb {
|
||||||
|
tonic::include_proto!("st_peter.auth");
|
||||||
|
}
|
||||||
|
|
||||||
|
use authpb::auth_service_client::AuthServiceClient;
|
||||||
|
use authpb::{
|
||||||
|
AuthenticatedUser, AuthenticationResponse, LoginRequest, LookupUserRequest, User,
|
||||||
|
VerifyTokenRequest, VerifyTwoFactorRequest,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
/// The token failed verification (or the response carried no user).
|
||||||
|
#[error("unauthorized")]
|
||||||
|
Unauthorized,
|
||||||
|
/// RPC-level failure talking to aura-users.
|
||||||
|
#[error(transparent)]
|
||||||
|
Rpc(#[from] tonic::Status),
|
||||||
|
/// The endpoint URL failed to parse (it must carry an `http://` or
|
||||||
|
/// `https://` scheme — a bare `host:port` does not).
|
||||||
|
#[error("invalid aura-users endpoint: {0}")]
|
||||||
|
InvalidUri(#[from] http::uri::InvalidUri),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TTL for cached token verifications (seconds). A revoked token stays
|
||||||
|
/// "valid" for up to this long — logout flows must not rely on the cache.
|
||||||
|
const AUTH_CACHE_TTL_SECS: u64 = 60;
|
||||||
|
|
||||||
|
/// Session cookie name issued by aura-users on login. Shared convention
|
||||||
|
/// across the aura services so a session works on any of them.
|
||||||
|
pub const COOKIE_NAME: &str = "aura_session";
|
||||||
|
|
||||||
|
/// Outcome of a `Login` (or `VerifyTwoFactor`) call against aura-users.
|
||||||
|
pub enum LoginOutcome {
|
||||||
|
/// Authenticated — carries the session (token + user + platform roles).
|
||||||
|
/// A login bridge sets the [`COOKIE_NAME`] cookie from `user.token`.
|
||||||
|
Authenticated(Box<AuthenticatedUser>),
|
||||||
|
/// aura-users issued a one-time passcode challenge (emailed/SMS'd). The
|
||||||
|
/// caller collects the code and calls
|
||||||
|
/// [`AuthClient::verify_two_factor`] with this `two_factor_id`.
|
||||||
|
TwoFactor { two_factor_id: String },
|
||||||
|
/// Login failed (bad credentials, locked out, …) — carries a display message.
|
||||||
|
Failed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map a st-peter `AuthenticationResponse` to a [`LoginOutcome`] (shared by
|
||||||
|
/// `login` and `verify_two_factor`): 2FA-required first, then success, else fail.
|
||||||
|
fn interpret_auth_response(resp: AuthenticationResponse) -> LoginOutcome {
|
||||||
|
if resp.pass_code_required {
|
||||||
|
return LoginOutcome::TwoFactor {
|
||||||
|
two_factor_id: resp.two_factor_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if resp.success {
|
||||||
|
if let Some(user) = resp.authenticated_user {
|
||||||
|
return LoginOutcome::Authenticated(Box::new(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoginOutcome::Failed(if resp.message.is_empty() {
|
||||||
|
"Invalid email or password.".to_string()
|
||||||
|
} else {
|
||||||
|
resp.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Thin client to aura-users' `AuthService`, with a small token-verify cache.
|
||||||
|
///
|
||||||
|
/// `AuthServiceClient<Channel>` is cheap to `clone` (a `Channel` is an
|
||||||
|
/// `Arc`'d connection handle), so we keep one and `.clone()` it per call
|
||||||
|
/// instead of locking a `&mut` client across an `.await`. `AuthClient`
|
||||||
|
/// itself is `Clone` and safe to share across tasks.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AuthClient {
|
||||||
|
inner: AuthServiceClient<Channel>,
|
||||||
|
cache: Arc<RwLock<HashMap<u64, (AuthenticatedUser, std::time::Instant)>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuthClient {
|
||||||
|
/// Connect to aura-users gRPC (internal address, e.g. `http://127.0.0.1:9091`).
|
||||||
|
///
|
||||||
|
/// The URL must carry an `http://`/`https://` scheme — a bare `host:port`
|
||||||
|
/// fails to parse. The dial is lazy with explicit timeouts, so a
|
||||||
|
/// slow/absent aura-users at boot doesn't block a consumer's startup;
|
||||||
|
/// failures surface on the first call instead.
|
||||||
|
pub async fn connect(grpc_url: &str) -> Result<Self> {
|
||||||
|
let channel = Channel::from_shared(grpc_url.to_string())?
|
||||||
|
.connect_timeout(std::time::Duration::from_secs(5))
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.connect_lazy();
|
||||||
|
let inner = AuthServiceClient::new(channel);
|
||||||
|
Ok(Self {
|
||||||
|
inner,
|
||||||
|
cache: Arc::new(RwLock::new(HashMap::new())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify a session token, returning the authenticated user (+ platform
|
||||||
|
/// roles). Cached ~60s to avoid a gRPC round-trip per request.
|
||||||
|
pub async fn verify_token(&self, token: &str) -> Result<AuthenticatedUser> {
|
||||||
|
let key = token_hash(token);
|
||||||
|
let ttl = std::time::Duration::from_secs(AUTH_CACHE_TTL_SECS);
|
||||||
|
|
||||||
|
// Read under the lock, clone out, and drop the guard before any `.await`.
|
||||||
|
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_token(VerifyTokenRequest {
|
||||||
|
token: token.to_string(),
|
||||||
|
include_user_roles: true, // platform roles; service roles stay local
|
||||||
|
role_scopes: vec![],
|
||||||
|
role_names: vec![],
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.into_inner();
|
||||||
|
|
||||||
|
// Gate on `success` first, then unwrap the option. The verified user
|
||||||
|
// is carried on `AuthenticationResponse.authenticated_user`.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Authenticate by identifier (email/phone) + password (`Login` RPC).
|
||||||
|
/// Returns `TwoFactor` when aura-users requires an OTP step,
|
||||||
|
/// `Authenticated` (with a session token) on success, or `Failed`.
|
||||||
|
pub async fn login(&self, identifier: &str, password: &str) -> Result<LoginOutcome> {
|
||||||
|
let mut client = self.inner.clone();
|
||||||
|
let resp = client
|
||||||
|
.login(LoginRequest {
|
||||||
|
user_identifier: identifier.to_string(),
|
||||||
|
password: password.to_string(),
|
||||||
|
app_hash: String::new(),
|
||||||
|
device_info: None,
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.into_inner();
|
||||||
|
Ok(interpret_auth_response(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Complete a two-factor challenge with the OTP code (`VerifyTwoFactor`
|
||||||
|
/// RPC), returning the authenticated session on success.
|
||||||
|
pub async fn verify_two_factor(&self, two_factor_id: &str, code: &str) -> Result<LoginOutcome> {
|
||||||
|
let mut client = self.inner.clone();
|
||||||
|
let resp = client
|
||||||
|
.verify_two_factor(VerifyTwoFactorRequest {
|
||||||
|
two_factor_id: two_factor_id.to_string(),
|
||||||
|
code: code.to_string(),
|
||||||
|
device_info: None,
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.into_inner();
|
||||||
|
Ok(interpret_auth_response(resp))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a st-peter user by exact identifier (email / phone / handle).
|
||||||
|
///
|
||||||
|
/// Typical use: a service-admin role-grant page — resolve a colleague's
|
||||||
|
/// email to a st-peter `user_id`, then store a local role row referencing
|
||||||
|
/// that id by value.
|
||||||
|
///
|
||||||
|
/// `LookupUser` requires an authenticated actor — pass the acting user's
|
||||||
|
/// own `user_id` + session `token`. Returns `None` when not found.
|
||||||
|
pub async fn lookup_user(
|
||||||
|
&self,
|
||||||
|
actor_user_id: &str,
|
||||||
|
actor_token: &str,
|
||||||
|
identifier: &str,
|
||||||
|
) -> Result<Option<User>> {
|
||||||
|
let mut client = self.inner.clone();
|
||||||
|
let resp = client
|
||||||
|
.lookup_user(LookupUserRequest {
|
||||||
|
user_id: actor_user_id.to_string(),
|
||||||
|
user_token: actor_token.to_string(),
|
||||||
|
identifier: identifier.to_string(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.into_inner();
|
||||||
|
if !resp.success {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Ok(resp.user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract a `Bearer <token>` from an `Authorization` header map. The shared
|
||||||
|
/// convention across aura services is: Bearer header first, then the
|
||||||
|
/// [`COOKIE_NAME`] session cookie.
|
||||||
|
pub fn bearer(headers: &http::HeaderMap) -> Option<String> {
|
||||||
|
headers
|
||||||
|
.get(http::header::AUTHORIZATION)?
|
||||||
|
.to_str()
|
||||||
|
.ok()?
|
||||||
|
.strip_prefix("Bearer ")
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fast (non-cryptographic) hash of the token, used only as the cache key.
|
||||||
|
fn token_hash(token: &str) -> u64 {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||||
|
token.hash(&mut h);
|
||||||
|
h.finish()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate Go gRPC stubs from the vendored proto into go/genpb/auth.
|
||||||
|
# The proto's in-file `option go_package` targets the server's module; the
|
||||||
|
# M-flag mapping here overrides it for this client module.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$here/go"
|
||||||
|
export PATH="$(go env GOPATH)/bin:$PATH"
|
||||||
|
MOD=git.awesomike.com/pub/st-peter-client/go
|
||||||
|
|
||||||
|
# proto filename (relative to ../proto) -> Go package dir under the module
|
||||||
|
protos=(
|
||||||
|
st-peter-auth.proto:genpb/auth
|
||||||
|
)
|
||||||
|
|
||||||
|
files=()
|
||||||
|
mflags=()
|
||||||
|
for entry in "${protos[@]}"; do
|
||||||
|
f="${entry%%:*}"; pkg="${entry##*:}"
|
||||||
|
files+=("$f")
|
||||||
|
mflags+=("--go_opt=M$f=$MOD/$pkg" "--go-grpc_opt=M$f=$MOD/$pkg")
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf genpb
|
||||||
|
protoc -I ../proto \
|
||||||
|
--go_out=. --go_opt=module="$MOD" \
|
||||||
|
--go-grpc_out=. --go-grpc_opt=module="$MOD" \
|
||||||
|
"${mflags[@]}" \
|
||||||
|
"${files[@]}"
|
||||||
|
|
||||||
|
echo "Generated Go stubs under go/genpb/"
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Generate TypeScript gRPC stubs from the vendored proto into ts/src/genpb,
|
||||||
|
# using ts-proto with @grpc/grpc-js service output.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$here/ts"
|
||||||
|
|
||||||
|
plugin="node_modules/.bin/protoc-gen-ts_proto"
|
||||||
|
if [[ ! -x "$plugin" ]]; then
|
||||||
|
echo "ts-proto not installed; run 'npm install' in ts/ first" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
out="src/genpb"
|
||||||
|
rm -rf "$out"; mkdir -p "$out"
|
||||||
|
|
||||||
|
protoc -I ../proto \
|
||||||
|
--plugin=protoc-gen-ts_proto="$plugin" \
|
||||||
|
--ts_proto_out="$out" \
|
||||||
|
--ts_proto_opt=outputServices=grpc-js,esModuleInterop=true,env=node,useExactTypes=false,unrecognizedEnum=false \
|
||||||
|
st-peter-auth.proto
|
||||||
|
|
||||||
|
echo "Generated TypeScript stubs under ts/src/genpb/"
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Sync the canonical .proto files from the st-peter server repo into this
|
||||||
|
# client repo's proto/ directory.
|
||||||
|
#
|
||||||
|
# The st-peter server repo is the SOURCE OF TRUTH for the wire contract.
|
||||||
|
# This repo vendors copies so the Go / TS / Rust clients can each generate
|
||||||
|
# stubs without depending on the server's Cargo workspace. Run this whenever
|
||||||
|
# the server's protos change, then re-run codegen and tag at the SAME version
|
||||||
|
# as the st-peter release (see VERSION).
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ST_PETER_REPO=/path/to/st-peter-lib ./scripts/sync-protos.sh
|
||||||
|
# Defaults to ../st-peter-lib relative to this repo.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
ST_PETER_REPO="${ST_PETER_REPO:-$here/../st-peter-lib}"
|
||||||
|
|
||||||
|
if [[ ! -f "$ST_PETER_REPO/Cargo.toml" ]]; then
|
||||||
|
echo "ERROR: st-peter repo not found at '$ST_PETER_REPO'." >&2
|
||||||
|
echo " Set ST_PETER_REPO=/path/to/st-peter-lib and re-run." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# canonical path in st-peter-lib -> filename in proto/
|
||||||
|
# NOTE: st-peter-admin.proto (the admin surface) and health.proto are
|
||||||
|
# intentionally NOT vendored — services authenticate users; they do not
|
||||||
|
# administer st-peter.
|
||||||
|
protos=(
|
||||||
|
"proto/st-peter-auth.proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
dest="$here/proto"
|
||||||
|
mkdir -p "$dest"
|
||||||
|
for rel in "${protos[@]}"; do
|
||||||
|
src="$ST_PETER_REPO/$rel"
|
||||||
|
[[ -f "$src" ]] || { echo "ERROR: missing $src" >&2; exit 1; }
|
||||||
|
cp "$src" "$dest/$(basename "$rel")"
|
||||||
|
echo "synced $(basename "$rel")"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Keep VERSION in lockstep with the st-peter workspace version.
|
||||||
|
sp_version="$(grep -m1 '^version' "$ST_PETER_REPO/Cargo.toml" | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||||
|
if [[ -n "$sp_version" ]]; then
|
||||||
|
echo "$sp_version" > "$here/VERSION"
|
||||||
|
echo "VERSION -> $sp_version (matched st-peter workspace)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Done. Re-run codegen (scripts/gen-*.sh) and tag at v$(cat "$here/VERSION")."
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# @st-peter/client (TypeScript)
|
||||||
|
|
||||||
|
Official TypeScript client for st-peter (aura-users). Generated stubs are
|
||||||
|
committed under `src/genpb/` (regenerate with `npm run gen`); the
|
||||||
|
`StPeterAuthClient` wrapper (token-verify cache, login/2FA/lookup) is layered
|
||||||
|
on top, with the raw wire surface re-exported as `authpb`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const auth = StPeterAuthClient.connect("127.0.0.1:9091");
|
||||||
|
const user = await auth.verifyToken(token); // cached ~60s
|
||||||
|
```
|
||||||
|
|
||||||
|
See the repo root README for versioning and the
|
||||||
|
authentication-central / authorization-local design.
|
||||||
|
|
@ -0,0 +1,447 @@
|
||||||
|
{
|
||||||
|
"name": "@st-peter/client",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@st-peter/client",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.12.0",
|
||||||
|
"long": "^5.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"ts-proto": "^2.6.0",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@bufbuild/protobuf": {
|
||||||
|
"version": "2.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz",
|
||||||
|
"integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(Apache-2.0 AND BSD-3-Clause)"
|
||||||
|
},
|
||||||
|
"node_modules/@grpc/grpc-js": {
|
||||||
|
"version": "1.14.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz",
|
||||||
|
"integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/proto-loader": "^0.8.0",
|
||||||
|
"@js-sdsl/ordered-map": "^4.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@grpc/proto-loader": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.camelcase": "^4.3.0",
|
||||||
|
"long": "^5.0.0",
|
||||||
|
"protobufjs": "^7.5.5",
|
||||||
|
"yargs": "^17.7.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@js-sdsl/ordered-map": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/aspromise": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/base64": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/codegen": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/eventemitter": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/fetch": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/float": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/inquire": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/path": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/pool": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@protobufjs/utf8": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "22.19.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.20.tgz",
|
||||||
|
"integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~6.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/case-anything": {
|
||||||
|
"version": "2.1.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.13.tgz",
|
||||||
|
"integrity": "sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.13"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/mesqueeb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cliui": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.1",
|
||||||
|
"wrap-ansi": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/detect-libc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"detect-libc": "bin/detect-libc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dprint-node": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/dprint-node/-/dprint-node-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/escalade": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-caller-file": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "6.* || 8.* || >= 10.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash.camelcase": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/protobufjs": {
|
||||||
|
"version": "7.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz",
|
||||||
|
"integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@protobufjs/aspromise": "^1.1.2",
|
||||||
|
"@protobufjs/base64": "^1.1.2",
|
||||||
|
"@protobufjs/codegen": "^2.0.5",
|
||||||
|
"@protobufjs/eventemitter": "^1.1.1",
|
||||||
|
"@protobufjs/fetch": "^1.1.1",
|
||||||
|
"@protobufjs/float": "^1.0.2",
|
||||||
|
"@protobufjs/inquire": "^1.1.2",
|
||||||
|
"@protobufjs/path": "^1.1.2",
|
||||||
|
"@protobufjs/pool": "^1.1.0",
|
||||||
|
"@protobufjs/utf8": "^1.1.1",
|
||||||
|
"@types/node": ">=13.7.0",
|
||||||
|
"long": "^5.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-directory": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-poet": {
|
||||||
|
"version": "6.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-poet/-/ts-poet-6.12.0.tgz",
|
||||||
|
"integrity": "sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"dprint-node": "^1.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-proto": {
|
||||||
|
"version": "2.11.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-2.11.8.tgz",
|
||||||
|
"integrity": "sha512-+5hzECnyVB33jxjG1BIdzAHcRBm7hjnm8womdJVp2A7xJWihP0drHHVsXYTr9i/LpWNGfh80I+AVVNzFM5AwJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@bufbuild/protobuf": "^2.10.2",
|
||||||
|
"case-anything": "^2.1.13",
|
||||||
|
"ts-poet": "^6.12.0",
|
||||||
|
"ts-proto-descriptors": "2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"protoc-gen-ts_proto": "protoc-gen-ts_proto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ts-proto-descriptors": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-proto-descriptors/-/ts-proto-descriptors-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@bufbuild/protobuf": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "6.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/y18n": {
|
||||||
|
"version": "5.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs": {
|
||||||
|
"version": "17.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||||
|
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^8.0.1",
|
||||||
|
"escalade": "^3.1.1",
|
||||||
|
"get-caller-file": "^2.0.5",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"string-width": "^4.2.3",
|
||||||
|
"y18n": "^5.0.5",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "@st-peter/client",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"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",
|
||||||
|
"type": "commonjs",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": ["dist", "src"],
|
||||||
|
"scripts": {
|
||||||
|
"gen": "../scripts/gen-ts.sh",
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"prepublishOnly": "npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@grpc/grpc-js": "^1.12.0",
|
||||||
|
"long": "^5.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-proto": "^2.6.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"@types/node": "^22.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* Official TypeScript client for st-peter (aura-users) — the central
|
||||||
|
* authentication service.
|
||||||
|
*
|
||||||
|
* Design: authentication central, authorization local. st-peter answers
|
||||||
|
* *who is this token?* — it returns the verified identity plus the user's
|
||||||
|
* platform roles. What that identity may do inside a consuming service is
|
||||||
|
* that service's own concern: keep a local roles table keyed by the
|
||||||
|
* st-peter user id by value (no cross-DB FK) and map permissions there.
|
||||||
|
* This package deliberately ships no session/permission types.
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* const auth = StPeterAuthClient.connect("127.0.0.1:9091");
|
||||||
|
* const user = await auth.verifyToken(token); // cached ~60s
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as grpc from "@grpc/grpc-js";
|
||||||
|
import {
|
||||||
|
AuthServiceClient,
|
||||||
|
AuthenticatedUser,
|
||||||
|
AuthenticationResponse,
|
||||||
|
LoginRequest,
|
||||||
|
LookupUserRequest,
|
||||||
|
LookupUserResponse,
|
||||||
|
User,
|
||||||
|
VerifyTokenRequest,
|
||||||
|
VerifyTwoFactorRequest,
|
||||||
|
} from "./genpb/st-peter-auth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session cookie name issued by aura-users on login. Shared convention
|
||||||
|
* across the aura services so a session works on any of them.
|
||||||
|
*/
|
||||||
|
export const COOKIE_NAME = "aura_session";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TTL for cached token verifications. A revoked token stays "valid" for up
|
||||||
|
* to this long — logout flows must not rely on the cache.
|
||||||
|
*/
|
||||||
|
const AUTH_CACHE_TTL_MS = 60_000;
|
||||||
|
|
||||||
|
/** Thrown when a token fails verification (or the response carries no user). */
|
||||||
|
export class UnauthorizedError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("unauthorized");
|
||||||
|
this.name = "UnauthorizedError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Outcome of a `login` (or `verifyTwoFactor`) call against aura-users. */
|
||||||
|
export type LoginOutcome =
|
||||||
|
/** Authenticated — a login bridge sets the COOKIE_NAME cookie from `user.token`. */
|
||||||
|
| { kind: "authenticated"; user: AuthenticatedUser }
|
||||||
|
/** aura-users issued an OTP challenge; collect the code, call `verifyTwoFactor`. */
|
||||||
|
| { kind: "two_factor"; twoFactorId: string }
|
||||||
|
/** Login failed (bad credentials, locked out, …) — carries a display message. */
|
||||||
|
| { kind: "failed"; message: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a st-peter AuthenticationResponse to a LoginOutcome (shared by
|
||||||
|
* `login` and `verifyTwoFactor`): 2FA-required first, then success, else fail.
|
||||||
|
*/
|
||||||
|
function interpretAuthResponse(resp: AuthenticationResponse): LoginOutcome {
|
||||||
|
if (resp.passCodeRequired) {
|
||||||
|
return { kind: "two_factor", twoFactorId: resp.twoFactorId };
|
||||||
|
}
|
||||||
|
if (resp.success && resp.authenticatedUser) {
|
||||||
|
return { kind: "authenticated", user: resp.authenticatedUser };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "failed",
|
||||||
|
message: resp.message || "Invalid email or password.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Options for `StPeterAuthClient.connect`. */
|
||||||
|
export interface ConnectOptions {
|
||||||
|
/** Channel credentials. Defaults to `grpc.credentials.createInsecure()`. */
|
||||||
|
credentials?: grpc.ChannelCredentials;
|
||||||
|
/** Extra gRPC channel options. */
|
||||||
|
channelOptions?: grpc.ChannelOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Promisify a unary grpc-js call. */
|
||||||
|
function call<Req, Res>(
|
||||||
|
fn: (req: Req, cb: (err: grpc.ServiceError | null, res: Res) => void) => unknown,
|
||||||
|
req: Req
|
||||||
|
): Promise<Res> {
|
||||||
|
return new Promise((resolve, reject) =>
|
||||||
|
fn(req, (err, res) => (err ? reject(err) : resolve(res)))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thin client to aura-users' AuthService with a small token-verify cache.
|
||||||
|
* Safe to share across async tasks — the underlying grpc-js channel is
|
||||||
|
* reference-counted.
|
||||||
|
*/
|
||||||
|
export class StPeterAuthClient {
|
||||||
|
private readonly svc: AuthServiceClient;
|
||||||
|
private readonly cache = new Map<string, { user: AuthenticatedUser; at: number }>();
|
||||||
|
|
||||||
|
private constructor(svc: AuthServiceClient) {
|
||||||
|
this.svc = svc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to aura-users gRPC (internal address, e.g. `127.0.0.1:9091` —
|
||||||
|
* grpc-js targets carry no scheme). The dial is lazy: failures surface on
|
||||||
|
* the first call, not at connect time.
|
||||||
|
*/
|
||||||
|
static connect(target: string, opts: ConnectOptions = {}): StPeterAuthClient {
|
||||||
|
const creds = opts.credentials ?? grpc.credentials.createInsecure();
|
||||||
|
return new StPeterAuthClient(new AuthServiceClient(target, creds, opts.channelOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tear down the underlying gRPC channel. */
|
||||||
|
close(): void {
|
||||||
|
this.svc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a session token, returning the authenticated user (+ platform
|
||||||
|
* roles). Cached ~60s to avoid a gRPC round-trip per request. Throws
|
||||||
|
* `UnauthorizedError` on a bad token.
|
||||||
|
*/
|
||||||
|
async verifyToken(token: string): Promise<AuthenticatedUser> {
|
||||||
|
const hit = this.cache.get(token);
|
||||||
|
if (hit && Date.now() - hit.at < AUTH_CACHE_TTL_MS) {
|
||||||
|
return hit.user;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await call<VerifyTokenRequest, AuthenticationResponse>(
|
||||||
|
this.svc.verifyToken.bind(this.svc),
|
||||||
|
VerifyTokenRequest.fromPartial({
|
||||||
|
token,
|
||||||
|
includeUserRoles: true, // platform roles; service roles stay local
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gate on `success` first, then the user. The verified user is carried
|
||||||
|
// on AuthenticationResponse.authenticated_user.
|
||||||
|
if (!resp.success || !resp.authenticatedUser) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
this.cache.set(token, { user: resp.authenticatedUser, at: Date.now() });
|
||||||
|
return resp.authenticatedUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Authenticate by identifier (email/phone) + password (`Login` RPC). */
|
||||||
|
async login(identifier: string, password: string): Promise<LoginOutcome> {
|
||||||
|
const resp = await call<LoginRequest, AuthenticationResponse>(
|
||||||
|
this.svc.login.bind(this.svc),
|
||||||
|
LoginRequest.fromPartial({ userIdentifier: identifier, password })
|
||||||
|
);
|
||||||
|
return interpretAuthResponse(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete a two-factor challenge with the OTP code, returning the
|
||||||
|
* authenticated session on success.
|
||||||
|
*/
|
||||||
|
async verifyTwoFactor(twoFactorId: string, code: string): Promise<LoginOutcome> {
|
||||||
|
const resp = await call<VerifyTwoFactorRequest, AuthenticationResponse>(
|
||||||
|
this.svc.verifyTwoFactor.bind(this.svc),
|
||||||
|
VerifyTwoFactorRequest.fromPartial({ twoFactorId, code })
|
||||||
|
);
|
||||||
|
return interpretAuthResponse(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a st-peter user by exact identifier (email / phone / handle).
|
||||||
|
* Requires an authenticated actor — pass the acting user's own user id +
|
||||||
|
* session token. Returns `undefined` when not found.
|
||||||
|
*/
|
||||||
|
async lookupUser(
|
||||||
|
actorUserId: string,
|
||||||
|
actorToken: string,
|
||||||
|
identifier: string
|
||||||
|
): Promise<User | undefined> {
|
||||||
|
const resp = await call<LookupUserRequest, LookupUserResponse>(
|
||||||
|
this.svc.lookupUser.bind(this.svc),
|
||||||
|
LookupUserRequest.fromPartial({
|
||||||
|
userId: actorUserId,
|
||||||
|
userToken: actorToken,
|
||||||
|
identifier,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return resp.success ? resp.user : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a `Bearer <token>` from an Authorization header value. The shared
|
||||||
|
* convention across aura services is: Bearer header first, then the
|
||||||
|
* COOKIE_NAME session cookie.
|
||||||
|
*/
|
||||||
|
export function bearer(authorization: string | undefined): string | undefined {
|
||||||
|
if (!authorization?.startsWith("Bearer ")) return undefined;
|
||||||
|
return authorization.slice("Bearer ".length).trim();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v7.34.1
|
||||||
|
// source: google/protobuf/timestamp.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
|
||||||
|
export const protobufPackage = "google.protobuf";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Timestamp represents a point in time independent of any time zone or local
|
||||||
|
* calendar, encoded as a count of seconds and fractions of seconds at
|
||||||
|
* nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||||
|
* January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||||
|
* Gregorian calendar backwards to year one.
|
||||||
|
*
|
||||||
|
* All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
|
||||||
|
* second table is needed for interpretation, using a [24-hour linear
|
||||||
|
* smear](https://developers.google.com/time/smear).
|
||||||
|
*
|
||||||
|
* The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
|
||||||
|
* restricting to that range, we ensure that we can convert to and from [RFC
|
||||||
|
* 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* Example 1: Compute Timestamp from POSIX `time()`.
|
||||||
|
*
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds(time(NULL));
|
||||||
|
* timestamp.set_nanos(0);
|
||||||
|
*
|
||||||
|
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||||
|
*
|
||||||
|
* struct timeval tv;
|
||||||
|
* gettimeofday(&tv, NULL);
|
||||||
|
*
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds(tv.tv_sec);
|
||||||
|
* timestamp.set_nanos(tv.tv_usec * 1000);
|
||||||
|
*
|
||||||
|
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||||
|
*
|
||||||
|
* FILETIME ft;
|
||||||
|
* GetSystemTimeAsFileTime(&ft);
|
||||||
|
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||||
|
*
|
||||||
|
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||||
|
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||||
|
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||||
|
*
|
||||||
|
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||||
|
*
|
||||||
|
* long millis = System.currentTimeMillis();
|
||||||
|
*
|
||||||
|
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||||
|
* .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||||
|
*
|
||||||
|
* Example 5: Compute Timestamp from Java `Instant.now()`.
|
||||||
|
*
|
||||||
|
* Instant now = Instant.now();
|
||||||
|
*
|
||||||
|
* Timestamp timestamp =
|
||||||
|
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||||
|
* .setNanos(now.getNano()).build();
|
||||||
|
*
|
||||||
|
* Example 6: Compute Timestamp from current time in Python.
|
||||||
|
*
|
||||||
|
* timestamp = Timestamp()
|
||||||
|
* timestamp.GetCurrentTime()
|
||||||
|
*
|
||||||
|
* # JSON Mapping
|
||||||
|
*
|
||||||
|
* In JSON format, the Timestamp type is encoded as a string in the
|
||||||
|
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||||
|
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||||
|
* where {year} is always expressed using four digits while {month}, {day},
|
||||||
|
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||||
|
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||||
|
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||||
|
* is required. A ProtoJSON serializer should always use UTC (as indicated by
|
||||||
|
* "Z") when printing the Timestamp type and a ProtoJSON parser should be
|
||||||
|
* able to accept both UTC and other timezones (as indicated by an offset).
|
||||||
|
*
|
||||||
|
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||||
|
* 01:30 UTC on January 15, 2017.
|
||||||
|
*
|
||||||
|
* In JavaScript, one can convert a Date object to this format using the
|
||||||
|
* standard
|
||||||
|
* [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||||
|
* method. In Python, a standard `datetime.datetime` object can be converted
|
||||||
|
* to this format using
|
||||||
|
* [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
|
||||||
|
* the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
|
||||||
|
* the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||||
|
* http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
|
||||||
|
* ) to obtain a formatter capable of generating timestamps in this format.
|
||||||
|
*/
|
||||||
|
export interface Timestamp {
|
||||||
|
/**
|
||||||
|
* Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must
|
||||||
|
* be between -62135596800 and 253402300799 inclusive (which corresponds to
|
||||||
|
* 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z).
|
||||||
|
*/
|
||||||
|
seconds: number;
|
||||||
|
/**
|
||||||
|
* Non-negative fractions of a second at nanosecond resolution. This field is
|
||||||
|
* the nanosecond portion of the duration, not an alternative to seconds.
|
||||||
|
* Negative second values with fractions must still have non-negative nanos
|
||||||
|
* values that count forward in time. Must be between 0 and 999,999,999
|
||||||
|
* inclusive.
|
||||||
|
*/
|
||||||
|
nanos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseTimestamp(): Timestamp {
|
||||||
|
return { seconds: 0, nanos: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Timestamp: MessageFns<Timestamp> = {
|
||||||
|
encode(message: Timestamp, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.seconds !== 0) {
|
||||||
|
writer.uint32(8).int64(message.seconds);
|
||||||
|
}
|
||||||
|
if (message.nanos !== 0) {
|
||||||
|
writer.uint32(16).int32(message.nanos);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Timestamp {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseTimestamp();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.seconds = longToNumber(reader.int64());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.nanos = reader.int32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Timestamp {
|
||||||
|
return {
|
||||||
|
seconds: isSet(object.seconds) ? globalThis.Number(object.seconds) : 0,
|
||||||
|
nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Timestamp): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.seconds !== 0) {
|
||||||
|
obj.seconds = Math.round(message.seconds);
|
||||||
|
}
|
||||||
|
if (message.nanos !== 0) {
|
||||||
|
obj.nanos = Math.round(message.nanos);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create(base?: DeepPartial<Timestamp>): Timestamp {
|
||||||
|
return Timestamp.fromPartial(base ?? {});
|
||||||
|
},
|
||||||
|
fromPartial(object: DeepPartial<Timestamp>): Timestamp {
|
||||||
|
const message = createBaseTimestamp();
|
||||||
|
message.seconds = object.seconds ?? 0;
|
||||||
|
message.nanos = object.nanos ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
export type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
function longToNumber(int64: { toString(): string }): number {
|
||||||
|
const num = globalThis.Number(int64.toString());
|
||||||
|
if (num > globalThis.Number.MAX_SAFE_INTEGER) {
|
||||||
|
throw new globalThis.Error("Value is larger than Number.MAX_SAFE_INTEGER");
|
||||||
|
}
|
||||||
|
if (num < globalThis.Number.MIN_SAFE_INTEGER) {
|
||||||
|
throw new globalThis.Error("Value is smaller than Number.MIN_SAFE_INTEGER");
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create(base?: DeepPartial<T>): T;
|
||||||
|
fromPartial(object: DeepPartial<T>): T;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,10 @@
|
||||||
|
export {
|
||||||
|
StPeterAuthClient,
|
||||||
|
UnauthorizedError,
|
||||||
|
COOKIE_NAME,
|
||||||
|
bearer,
|
||||||
|
} from "./auth";
|
||||||
|
export type { LoginOutcome, ConnectOptions } from "./auth";
|
||||||
|
|
||||||
|
// Raw wire surface for callers that need it.
|
||||||
|
export * as authpb from "./genpb/st-peter-auth";
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue