feat(auth): instance-owner gate — owner store, JWT sessions, setup/login UI (onboarding M1+M2)

lib/owner: argon2id credential store, HS256 access/refresh pair with kid,
hashed-jti session store, throttler (5 free failures then 4^n backoff).
handlers/auth: /api/auth/* + RequireSession/CSRF/GateWebSocket middleware;
every /api/* route is now gated (428 in setup mode). --reset-owner recovery.
Frontend: AuthScreen (setup/login), useAuth gate hook, gated data hooks +
single-flight silent refresh, Owner account section in settings.
spec.yaml documents the full surface.
This commit is contained in:
2026-08-22 22:58:18 +01:00
parent cf51a6be94
commit 2ffdf77905
24 changed files with 2850 additions and 37 deletions
+182 -2
View File
@@ -2,7 +2,7 @@ openapi: 3.0.3
info:
title: Dcntrlized Maestro API
version: 1.0.0
description: The API of the Maestro orchestration router. Maestro manages the lifecycle of Dcntrlized service containers and routes inter-service action calls to them.
description: The API of the Maestro orchestration router. Maestro manages the lifecycle of Dcntrlized service containers and routes inter-service action calls to them. Every /api/* route except /api/auth/* requires an instance-owner session (cookies issued by the auth endpoints); in setup mode gated routes answer 428 ERR_SETUP_REQUIRED, otherwise 401 ERR_UNAUTHENTICATED. Mutating requests from browser origins are additionally checked against the request host (CSRF).
components:
schemas:
@@ -29,6 +29,65 @@ components:
message:
type: string
ApiError:
type: object
description: The uniform API error shape (onboarding theme M1).
properties:
error:
type: object
properties:
code:
type: string
example: "ERR_UNAUTHENTICATED"
AuthStatus:
type: object
description: The instance-owner gate state; the SPA bootstraps from it.
properties:
setup_required:
type: boolean
description: True until the first owner account exists.
authenticated:
type: boolean
username:
type: string
Credentials:
type: object
required: [username, password]
properties:
username:
type: string
description: "3-64 chars: lowercase letters, digits and . _ - ; must start alphanumeric."
password:
type: string
minLength: 10
SessionResponse:
type: object
description: >-
Login/setup/rotation result. The access and refresh tokens ride as
HttpOnly SameSite=Lax cookies (maestro_access / maestro_refresh).
properties:
username:
type: string
role:
type: string
example: "admin"
access_ttl_s:
type: integer
example: 900
PasswordChange:
type: object
required: [current_password, new_password]
properties:
current_password:
type: string
new_password:
type: string
minLength: 10
PendingAuthorization:
type: object
description: A first-contact authorization request awaiting the instance owner (M2 §3).
@@ -447,14 +506,135 @@ paths:
'502':
description: Failed to connect to upstream registry
/api/auth/status:
get:
summary: Instance-owner gate state (public)
tags: ["auth"]
description: >-
The SPA bootstraps from this endpoint. All other /api/* routes are
gated by the instance-owner session (onboarding theme M1): in setup
mode they answer 428 ERR_SETUP_REQUIRED, otherwise 401
ERR_UNAUTHENTICATED without a valid session.
responses:
'200':
description: Gate state
content:
application/json:
schema:
$ref: '#/components/schemas/AuthStatus'
'500':
description: Internal server error
/api/auth/setup:
post:
summary: Create the first owner account (admin)
tags: ["auth"]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Credentials'
responses:
'200':
description: Owner created; session cookies issued
content:
application/json:
schema:
$ref: '#/components/schemas/SessionResponse'
'400':
description: Invalid credentials (ERR_INVALID_REQUEST / ERR_INVALID_CREDENTIALS)
'409':
description: Setup already completed (ERR_SETUP_COMPLETED)
'429':
description: Rate limited (ERR_RATE_LIMITED)
/api/auth/login:
post:
summary: Log in as the instance owner
tags: ["auth"]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Credentials'
responses:
'200':
description: Session cookies issued
content:
application/json:
schema:
$ref: '#/components/schemas/SessionResponse'
'400':
description: Invalid JSON payload
'401':
description: Unknown user or wrong password (ERR_INVALID_CREDENTIALS)
'429':
description: Rate limited (ERR_RATE_LIMITED) with Retry-After
/api/auth/refresh:
post:
summary: Rotate the access/refresh pair
tags: ["auth"]
description: >-
Uses the maestro_refresh cookie. Reuse of an already-rotated jti is a
theft signal and revokes every session of the user
(ERR_SESSION_REVOKED).
responses:
'200':
description: Fresh pair issued
content:
application/json:
schema:
$ref: '#/components/schemas/SessionResponse'
'401':
description: Missing/invalid/replayed refresh cookie
/api/auth/logout:
post:
summary: Revoke the current refresh token and clear cookies
tags: ["auth"]
responses:
'200':
description: Logged out (idempotent, bodyless-safe)
/api/auth/change-password:
post:
summary: Change the owner's password (session required)
tags: ["auth"]
description: Revokes every existing session of the user on success.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordChange'
responses:
'200':
description: Password changed; all sessions revoked
'400':
description: Invalid payload or wrong current password
'401':
description: Not authenticated
'429':
description: Rate limited (ERR_RATE_LIMITED)
/api/ws:
get:
summary: WebSocket endpoint for live container events
tags: ["containers"]
description: Establishes a WebSocket connection that streams container lifecycle events as JSON.
description: >-
Establishes a WebSocket connection that streams container lifecycle
events as JSON. The upgrade is gated by the instance-owner session;
anonymous handshakes close with 401 (428 in setup mode).
responses:
'101':
description: Switching Protocols - WebSocket upgrade
'401':
description: Not authenticated
'428':
description: Setup required
/api/peers:
get: