plan: add Architecture.md (tier model, flows, data model, packaging)
+525
@@ -0,0 +1,525 @@
|
||||
# Architecture
|
||||
|
||||
**Status:** Draft v1
|
||||
**Date:** 2026-06-24
|
||||
**Owner:** Patrick (plate-software)
|
||||
|
||||
---
|
||||
|
||||
## 1. Two artifacts, one contract
|
||||
|
||||
plate-auth ships as **two coordinated artifacts** that share a single wire contract (the HMAC-signed
|
||||
exchange envelope + the JWT shape).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Backend["plate-auth-starter (Maven)"]
|
||||
BE[de.platesoft.auth.*<br/>Spring Boot 4 auto-config]
|
||||
end
|
||||
subgraph Frontend["@platesoft/auth (npm)"]
|
||||
FE[NextAuth v5 wiring +<br/>exchange + proxy helpers]
|
||||
end
|
||||
Wire[(Wire contract:<br/>POST /api/auth/exchange<br/>HMAC-SHA256 envelope<br/>JWT bearer)]
|
||||
FE -.signs.-> Wire
|
||||
BE -.verifies.-> Wire
|
||||
```
|
||||
|
||||
The wire contract is the **only thing the two artifacts know about each other**. Either side can be
|
||||
re-implemented in another language as long as the contract is honored.
|
||||
|
||||
---
|
||||
|
||||
## 2. Tier model — what is in, what is out
|
||||
|
||||
We split InspectFlow's auth/membership system into three tiers. plate-auth ships **T1 + T2 only**.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph T1["T1 — Auth Core (plate-auth-starter)"]
|
||||
T1a[User + UserIdentity entities]
|
||||
T1b[JwtAuthenticationFilter]
|
||||
T1c[JwtService]
|
||||
T1d[SecurityConfig auto-config]
|
||||
T1e[OAuthController + ExchangeService]
|
||||
T1f[AuthController login/register/refresh/me]
|
||||
T1g[LoginEventService + revinfo actor]
|
||||
end
|
||||
subgraph T2["T2 — Multi-Tenancy (plate-auth-starter)"]
|
||||
T2a[Membership entity + service]
|
||||
T2b[Invitation entity + service + controller]
|
||||
T2c[AccessRequest entity + service + controller]
|
||||
T2d[OrgContextResolver]
|
||||
T2e[AdminAuditController]
|
||||
end
|
||||
subgraph T3["T3 — Stays in consuming app"]
|
||||
T3a[Company / Workspace / Club<br/>concrete org entity]
|
||||
T3b[OnboardingService<br/>business-state machine]
|
||||
T3c[TenantAutoMapService<br/>MS-tenant → company mapping]
|
||||
T3d[Org-specific embeddings,<br/>industry codes, etc.]
|
||||
end
|
||||
|
||||
T2 -.references via FK.-> T3
|
||||
T2 -.OrgContextResolver hook.-> T3
|
||||
```
|
||||
|
||||
**Why this cut:**
|
||||
|
||||
- **T1** has zero domain coupling — works for any product.
|
||||
- **T2** is "user × org × role" — also generic, but its FK target (`org_id`) is per-product.
|
||||
- **T3** is product-specific business logic. It would constrain consumers if we extracted it.
|
||||
|
||||
The seam between T2 and T3 is an **`OrgValidator` SPI** (Service Provider Interface) that the consuming app
|
||||
implements:
|
||||
|
||||
```java
|
||||
public interface OrgValidator {
|
||||
boolean exists(OrgType type, UUID orgId);
|
||||
String displayName(OrgType type, UUID orgId);
|
||||
}
|
||||
```
|
||||
|
||||
plate-auth's `MembershipService.grant(…)` calls `orgValidator.exists(…)` instead of joining to a `companies`
|
||||
table directly. This is the only generic-↔-domain coupling point.
|
||||
|
||||
---
|
||||
|
||||
## 3. Public API surface (Spring Boot starter)
|
||||
|
||||
What a consumer can call / configure / override. Everything else is `package-private` or `@Internal`.
|
||||
|
||||
### 3.1 HTTP endpoints (mounted under `/api/auth/**` and `/api/`)
|
||||
|
||||
| Endpoint | Method | Public? | Purpose |
|
||||
|---|---|---|---|
|
||||
| `/api/auth/exchange` | POST | Public (HMAC-signed) | NextAuth → backend session bootstrap |
|
||||
| `/api/auth/register` | POST | Public (if `plate.auth.registration.enabled=true`) | Password registration |
|
||||
| `/api/auth/login` | POST | Public | Password login |
|
||||
| `/api/auth/refresh` | POST | Public | Refresh token rotation |
|
||||
| `/api/auth/logout` | POST | Public | Refresh token revocation |
|
||||
| `/api/auth/me` | GET | Authenticated | Current user + memberships |
|
||||
| `/api/auth/config` | GET | Public | Provider list + registration flag |
|
||||
| `/api/invitations` | CRUD | Authenticated (admin) | Invite management |
|
||||
| `/api/invitations/accept` | POST | Authenticated | Accept invite |
|
||||
| `/api/access-requests` | CRUD | Authenticated (admin) | Access request management |
|
||||
| `/api/admin/login-events` | GET | `hasRole('ADMIN')` | Audit log |
|
||||
| `/api/admin/audit/revisions` | GET | `hasRole('ADMIN')` | Envers revision browser |
|
||||
|
||||
### 3.2 Spring beans the consumer can inject
|
||||
|
||||
| Bean | Purpose |
|
||||
|---|---|
|
||||
| [`JwtService`](Architecture.md:62) | Generate / parse JWTs. Issue tokens for arbitrary `User`. |
|
||||
| `OAuthService` | Find-or-create User+Identity from exchange payload. |
|
||||
| `MembershipService` | Grant / revoke / list memberships. |
|
||||
| `InvitationService` | Create / accept / revoke invitations. |
|
||||
| `AccessRequestService` | Submit / approve / deny access requests. |
|
||||
| `LoginEventService` | Record login attempts (async). |
|
||||
|
||||
### 3.3 Configuration properties (`plate.auth.*`)
|
||||
|
||||
```yaml
|
||||
plate:
|
||||
auth:
|
||||
jwt:
|
||||
secret: ${PLATE_AUTH_JWT_SECRET} # ≥32 chars, HMAC-SHA256 key
|
||||
access-expiration: PT15M # ISO-8601 duration
|
||||
refresh-expiration: P30D
|
||||
issuer: plate-auth
|
||||
exchange:
|
||||
secret: ${PLATE_AUTH_EXCHANGE_SECRET} # ≥32 chars, shared with frontend
|
||||
max-age: PT60S # envelope freshness window
|
||||
nonce-ttl: PT5M # replay-protection window
|
||||
registration:
|
||||
enabled: false # password registration on/off
|
||||
cors:
|
||||
allowed-origins: # space-separated or list
|
||||
- https://app.example.com
|
||||
providers:
|
||||
google:
|
||||
enabled: true
|
||||
microsoft:
|
||||
enabled: false # Entra ID — opt-in
|
||||
email-magic-link:
|
||||
enabled: false # NextAuth Email provider
|
||||
```
|
||||
|
||||
### 3.4 Extension points (SPI)
|
||||
|
||||
Consumers implement these to plug their domain into plate-auth:
|
||||
|
||||
| Interface | Required? | Default | Purpose |
|
||||
|---|---|---|---|
|
||||
| `OrgValidator` | **Yes** (if T2 used) | — | Validate `org_id` exists in consumer's `companies` / `workspaces` / etc. |
|
||||
| `OrgDisplayNameResolver` | Optional | Returns `org_id.toString()` | Pretty-print org for invitations / emails. |
|
||||
| `InvitationMailer` | Optional | No-op logger | Send invitation emails. Default just logs the token URL. |
|
||||
| `AccessRequestMailer` | Optional | No-op logger | Notify admins of new access requests. |
|
||||
| `OnboardingHook` | Optional | No-op | Called on first successful sign-in — consumer's place to wire T3 onboarding. |
|
||||
|
||||
All extension points are `@ConditionalOnMissingBean` — register your own bean to override.
|
||||
|
||||
---
|
||||
|
||||
## 4. Public API surface (npm package)
|
||||
|
||||
### 4.1 Exports
|
||||
|
||||
```ts
|
||||
// @platesoft/auth/config
|
||||
export function createAuthConfig(opts: PlateAuthConfigOptions): NextAuthConfig;
|
||||
|
||||
// @platesoft/auth/exchange
|
||||
export async function exchangeWithBackend(envelope: ExchangeEnvelope): Promise<TokenResponse>;
|
||||
export function signEnvelope(envelope: object, secret: string): { signature: string; envelope: string };
|
||||
|
||||
// @platesoft/auth/proxy
|
||||
export function createProxyHandlers(opts: ProxyOptions): {
|
||||
GET: RouteHandler; POST: RouteHandler; PUT: RouteHandler; PATCH: RouteHandler; DELETE: RouteHandler;
|
||||
};
|
||||
|
||||
// @platesoft/auth/middleware
|
||||
export function createAuthMiddleware(opts?: MiddlewareOptions): NextMiddleware;
|
||||
|
||||
// @platesoft/auth/client
|
||||
export { useSession, signIn, signOut } from 'next-auth/react'; // re-export
|
||||
export function useAccessToken(): string | null;
|
||||
export function useMemberships(): Membership[];
|
||||
```
|
||||
|
||||
### 4.2 Required env vars
|
||||
|
||||
```bash
|
||||
# Frontend (Next.js)
|
||||
NEXTAUTH_SECRET=... # NextAuth session secret
|
||||
NEXTAUTH_EXCHANGE_SECRET=... # MUST match backend plate.auth.exchange.secret
|
||||
NEXT_PUBLIC_BACKEND_URL=https://api.example.com
|
||||
GOOGLE_CLIENT_ID=...
|
||||
GOOGLE_CLIENT_SECRET=...
|
||||
# Optional
|
||||
MICROSOFT_ENTRA_CLIENT_ID=...
|
||||
MICROSOFT_ENTRA_CLIENT_SECRET=...
|
||||
MICROSOFT_ENTRA_TENANT_ID=common
|
||||
EMAIL_SERVER=... # only if magic-link enabled
|
||||
EMAIL_FROM=...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. End-to-end sign-in flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant FE as Browser (Next.js)
|
||||
participant NA as NextAuth (/api/auth/[...nextauth])
|
||||
participant G as Google
|
||||
participant BE as Spring Boot (plate-auth)
|
||||
participant DB as Postgres
|
||||
|
||||
U->>FE: click "Sign in with Google"
|
||||
FE->>NA: signIn('google')
|
||||
NA->>G: redirect to Google OAuth
|
||||
G-->>NA: callback with id_token
|
||||
NA->>NA: signIn callback fires
|
||||
NA->>NA: build ExchangeEnvelope<br/>{provider, providerSubject, email, name, nonce, iat}
|
||||
NA->>NA: sign envelope with HMAC-SHA256(NEXTAUTH_EXCHANGE_SECRET)
|
||||
NA->>BE: POST /api/auth/exchange<br/>X-Exchange-Signature: <hmac><br/>body: envelope
|
||||
BE->>BE: ExchangeService.verifyAndParse()<br/>(HMAC verify, max-age 60s, nonce dedup)
|
||||
BE->>DB: SELECT user_identity WHERE provider+subject
|
||||
alt Identity exists
|
||||
DB-->>BE: User row
|
||||
else New identity
|
||||
BE->>DB: INSERT user + user_identity
|
||||
BE->>BE: OnboardingHook.onFirstSignIn(user)
|
||||
end
|
||||
BE->>DB: INSERT login_event (success)
|
||||
BE-->>NA: 200 {access_token, refresh_token, user, memberships}
|
||||
NA->>NA: store tokens in NextAuth JWT
|
||||
NA-->>FE: set session cookie
|
||||
FE-->>U: redirect to app
|
||||
```
|
||||
|
||||
Key invariants:
|
||||
|
||||
- **Step 5 (signIn callback)** is where NextAuth's "I authenticated this user" claim becomes plate-auth's
|
||||
"I issue tokens for this user" — bridged by the HMAC envelope.
|
||||
- **Step 7 (HMAC verify)** is the only thing preventing a malicious caller from minting tokens for any
|
||||
Google account.
|
||||
- **Step 8 (nonce dedup)** is the only thing preventing replay of a captured envelope within its 60s
|
||||
freshness window.
|
||||
- **Step 13 (`OnboardingHook`)** is the T3 escape hatch — consumers wire whatever first-login flow they need.
|
||||
|
||||
---
|
||||
|
||||
## 6. Request → API flow (authenticated)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
actor U as User
|
||||
participant FE as Browser
|
||||
participant PR as Next.js /api/[...path] proxy
|
||||
participant NA as NextAuth auth()
|
||||
participant BE as Spring Boot
|
||||
participant JF as JwtAuthFilter
|
||||
participant OR as OrgContextResolver
|
||||
participant CT as @RestController
|
||||
|
||||
U->>FE: action triggers fetch /api/things
|
||||
FE->>PR: GET /api/things (no auth header — session cookie only)
|
||||
PR->>NA: auth() — read session
|
||||
NA-->>PR: {accessToken, user}
|
||||
PR->>BE: GET /api/things<br/>Authorization: Bearer <accessToken><br/>X-Org-Id: <selectedOrg> (optional)
|
||||
BE->>JF: JwtAuthFilter.doFilter()
|
||||
JF->>JF: extract + verify JWT (HMAC SHA-256, issuer check)
|
||||
JF->>JF: set SecurityContext (principal = email, authorities = [ROLE_<role>])
|
||||
JF->>OR: OrgContextResolver.resolve()
|
||||
OR->>OR: parse X-Org-Id, validate membership via MembershipService
|
||||
OR->>OR: set OrgContext threadlocal
|
||||
OR->>CT: forward to controller
|
||||
CT-->>BE: response
|
||||
BE-->>PR: 200 response (or 401/403)
|
||||
PR-->>FE: passthrough response
|
||||
```
|
||||
|
||||
The proxy pattern (step 2-6) is the key design choice: **the browser never sees the JWT directly**. The
|
||||
JWT lives in the encrypted NextAuth session cookie; the proxy adds the `Authorization` header server-side
|
||||
before forwarding. This sidesteps XSS token theft.
|
||||
|
||||
---
|
||||
|
||||
## 7. Data model
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS ||--o{ USER_IDENTITIES : "1:N"
|
||||
USERS ||--o{ MEMBERSHIPS : "1:N"
|
||||
USERS ||--o{ INVITATIONS : "created_by"
|
||||
USERS ||--o{ ACCESS_REQUESTS : "requested_by"
|
||||
USERS ||--o{ LOGIN_EVENTS : "actor"
|
||||
MEMBERSHIPS }o..|| ORG_TABLE : "(org_type, org_id)<br/>polymorphic FK"
|
||||
INVITATIONS }o..|| ORG_TABLE : "polymorphic"
|
||||
ACCESS_REQUESTS }o..|| ORG_TABLE : "polymorphic"
|
||||
|
||||
USERS {
|
||||
uuid id PK
|
||||
text email UK
|
||||
text password_hash "nullable for OAuth-only"
|
||||
text first_name
|
||||
text last_name
|
||||
text role "ROLE_USER | ROLE_ADMIN"
|
||||
boolean active
|
||||
uuid default_org_id "nullable"
|
||||
text last_provider "google|microsoft|email|password"
|
||||
bigint version "optimistic lock"
|
||||
}
|
||||
USER_IDENTITIES {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
text provider "google|microsoft|email|password"
|
||||
text subject "provider's stable user id"
|
||||
text email
|
||||
text tenant_id "MS Entra tenant, nullable"
|
||||
timestamptz linked_at
|
||||
timestamptz last_login_at
|
||||
}
|
||||
MEMBERSHIPS {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
text org_type "COMPANY|WORKSPACE|CLUB|..."
|
||||
uuid org_id "polymorphic — validated via OrgValidator SPI"
|
||||
text role "OWNER|ADMIN|MEMBER|VIEWER"
|
||||
text status "ACTIVE|SUSPENDED|REVOKED"
|
||||
uuid granted_by FK
|
||||
text grant_reason
|
||||
timestamptz revoked_at
|
||||
uuid revoked_by FK
|
||||
}
|
||||
INVITATIONS {
|
||||
uuid id PK
|
||||
text token UK "64-char single-use"
|
||||
text email
|
||||
text org_type
|
||||
uuid org_id
|
||||
text role
|
||||
text status "PENDING|ACCEPTED|REVOKED|EXPIRED"
|
||||
uuid created_by FK
|
||||
timestamptz created_at
|
||||
timestamptz expires_at
|
||||
timestamptz accepted_at
|
||||
}
|
||||
ACCESS_REQUESTS {
|
||||
uuid id PK
|
||||
uuid requester_id FK
|
||||
text org_type
|
||||
uuid org_id
|
||||
text requested_role "default VIEWER"
|
||||
text justification
|
||||
text status "PENDING|APPROVED|DENIED|EXPIRED"
|
||||
uuid reviewer_id FK
|
||||
text decision_reason
|
||||
}
|
||||
LOGIN_EVENTS {
|
||||
bigint id PK
|
||||
uuid user_id FK
|
||||
text email "denormalized for failed attempts"
|
||||
text provider
|
||||
text outcome "SUCCESS|FAILURE|LOCKED"
|
||||
text ip_address
|
||||
text user_agent
|
||||
text correlation_id
|
||||
timestamptz occurred_at
|
||||
}
|
||||
```
|
||||
|
||||
**The polymorphic FK on `memberships.org_id`** is the only "loose" join in the schema. It is intentional:
|
||||
plate-auth must not require a specific concrete org table. Validation is enforced via:
|
||||
|
||||
1. The `OrgValidator` SPI (application-level check at membership-grant time).
|
||||
2. The `fn_membership_org_fk()` trigger (consumer-defined — InspectFlow's trigger validates against
|
||||
`companies`; Sparkboard's will validate against `workspaces`).
|
||||
|
||||
---
|
||||
|
||||
## 8. Package layout
|
||||
|
||||
### 8.1 Maven module
|
||||
|
||||
```
|
||||
plate-auth/
|
||||
├── plate-auth-starter/ # the published artifact
|
||||
│ ├── pom.xml # de.platesoft:plate-auth-starter:0.1.0
|
||||
│ └── src/main/java/de/platesoft/auth/
|
||||
│ ├── PlateAuthAutoConfiguration.java
|
||||
│ ├── PlateAuthProperties.java
|
||||
│ ├── config/
|
||||
│ │ └── SecurityConfig.java
|
||||
│ ├── filter/
|
||||
│ │ ├── JwtAuthenticationFilter.java
|
||||
│ │ └── OrgContextResolver.java
|
||||
│ ├── service/
|
||||
│ │ ├── JwtService.java
|
||||
│ │ ├── OAuthService.java
|
||||
│ │ ├── MembershipService.java
|
||||
│ │ ├── InvitationService.java
|
||||
│ │ ├── AccessRequestService.java
|
||||
│ │ ├── LoginEventService.java
|
||||
│ │ └── ExchangeService.java
|
||||
│ ├── controller/
|
||||
│ │ ├── AuthController.java
|
||||
│ │ ├── OAuthController.java
|
||||
│ │ ├── InvitationController.java
|
||||
│ │ ├── AccessRequestController.java
|
||||
│ │ └── AdminAuditController.java
|
||||
│ ├── entity/
|
||||
│ │ ├── User.java
|
||||
│ │ ├── UserIdentity.java
|
||||
│ │ ├── Membership.java
|
||||
│ │ ├── Invitation.java
|
||||
│ │ ├── AccessRequest.java
|
||||
│ │ └── LoginEvent.java
|
||||
│ ├── repository/...
|
||||
│ ├── dto/
|
||||
│ │ ├── request/...
|
||||
│ │ └── response/...
|
||||
│ ├── spi/
|
||||
│ │ ├── OrgValidator.java
|
||||
│ │ ├── OrgDisplayNameResolver.java
|
||||
│ │ ├── InvitationMailer.java
|
||||
│ │ ├── AccessRequestMailer.java
|
||||
│ │ └── OnboardingHook.java
|
||||
│ └── enums/
|
||||
│ ├── OrgType.java
|
||||
│ ├── MembershipRole.java
|
||||
│ ├── MembershipStatus.java
|
||||
│ ├── InvitationStatus.java
|
||||
│ ├── AccessRequestStatus.java
|
||||
│ ├── LoginProvider.java
|
||||
│ └── Role.java
|
||||
│ └── resources/
|
||||
│ ├── META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
|
||||
│ └── db/migration/auth/
|
||||
│ ├── V1__create_users_and_identities.sql
|
||||
│ ├── V2__create_memberships.sql
|
||||
│ ├── V3__create_invitations.sql
|
||||
│ ├── V4__create_access_requests.sql
|
||||
│ ├── V5__add_microsoft_tenant_id_index.sql
|
||||
│ └── V6__create_login_events_and_revinfo_actor.sql
|
||||
└── plate-auth-tests/ # internal integration tests, not published
|
||||
```
|
||||
|
||||
### 8.2 npm package
|
||||
|
||||
```
|
||||
@platesoft/auth/
|
||||
├── package.json # @platesoft/auth@0.1.0
|
||||
├── src/
|
||||
│ ├── config/
|
||||
│ │ ├── index.ts # createAuthConfig()
|
||||
│ │ ├── providers/
|
||||
│ │ │ ├── google.ts
|
||||
│ │ │ ├── microsoft.ts
|
||||
│ │ │ └── email.ts
|
||||
│ │ └── callbacks/
|
||||
│ │ ├── signIn.ts # exchange-with-backend signIn
|
||||
│ │ └── jwt.ts
|
||||
│ ├── exchange/
|
||||
│ │ ├── envelope.ts # signEnvelope, verifyEnvelope (Edge-compatible HMAC)
|
||||
│ │ └── client.ts # exchangeWithBackend
|
||||
│ ├── proxy/
|
||||
│ │ └── handlers.ts # createProxyHandlers
|
||||
│ ├── middleware/
|
||||
│ │ └── index.ts # createAuthMiddleware
|
||||
│ └── client/
|
||||
│ ├── hooks.ts # useAccessToken, useMemberships
|
||||
│ └── index.ts # re-exports from next-auth/react
|
||||
└── tests/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Distribution
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Source[plate-auth git repo<br/>git.plate-software.de/pplate/plate-auth] --> CI{Gitea Actions}
|
||||
CI -->|tag v0.x.y| MavenReg[Gitea Package Registry<br/>Maven]
|
||||
CI -->|tag v0.x.y| NpmReg[Gitea Package Registry<br/>npm]
|
||||
MavenReg --> IF1[InspectFlow backend]
|
||||
MavenReg --> SB1[Sparkboard backend]
|
||||
NpmReg --> IF2[InspectFlow frontend]
|
||||
NpmReg --> SB2[Sparkboard frontend]
|
||||
```
|
||||
|
||||
- Single git repo, two artifact pipelines.
|
||||
- Tag `v0.1.0` triggers publish of both `de.platesoft:plate-auth-starter:0.1.0` and `@platesoft/auth@0.1.0`.
|
||||
**Versions stay locked in lockstep** — frontend 0.2.0 implies backend 0.2.0 is the required peer.
|
||||
- Gitea Package Registry is the only distribution channel. No public registry.
|
||||
|
||||
---
|
||||
|
||||
## 10. Threat model summary
|
||||
|
||||
| Threat | Mitigation |
|
||||
|---|---|
|
||||
| Forged exchange envelope | HMAC-SHA256 with ≥32-char shared secret, constant-time compare |
|
||||
| Replay of captured envelope | 60s `max-age` window + 5min nonce dedup store |
|
||||
| JWT theft via XSS | Token never sent to browser — proxy injects it server-side |
|
||||
| JWT forgery | HMAC-SHA256, ≥32-char secret, issuer + expiration validation |
|
||||
| Membership escalation | Rank-based `MembershipService.grant()` — cannot grant higher role than your own |
|
||||
| Invitation token enumeration | 64-char URL-safe random, single-use, expires after 7d (configurable) |
|
||||
| Access request DoS | Rate-limit 3 per user per day |
|
||||
| Replay across requests | JWT is short-lived (15min); refresh tokens are rotated |
|
||||
| Audit gap | All identity-changing ops emit revinfo with `actor_user_id`; failed logins emit `login_event` |
|
||||
|
||||
Full threat model lives in `Sprint-0-Plan.md` § "Security review checklist".
|
||||
|
||||
---
|
||||
|
||||
## 11. What this document is not
|
||||
|
||||
- ❌ Not a step-by-step extraction plan → see [`Sprint-0-Plan.md`](Sprint-0-Plan.md)
|
||||
- ❌ Not a per-API specification → generated from JavaDoc / TSDoc at v0.2
|
||||
- ❌ Not the threat model in full → see Sprint-0-Plan § Security
|
||||
|
||||
It is the **shape** of the system. The rest of the wiki fills in the pixels.
|
||||
Reference in New Issue
Block a user