From cecc6bfa42d079f91240a7e10b66243840123be5 Mon Sep 17 00:00:00 2001 From: Karolis Strazdas Date: Sun, 8 Nov 2026 00:17:55 +0200 Subject: [PATCH 1/2] docs: replace implementation proposals with current contracts --- conf.py | 1 - design/server-architecture.rst | 41 ++- proposals/authentication.rst | 463 --------------------------------- proposals/index.rst | 19 -- 4 files changed, 31 insertions(+), 493 deletions(-) delete mode 100644 proposals/authentication.rst delete mode 100644 proposals/index.rst diff --git a/conf.py b/conf.py index 4954a29..f0af331 100644 --- a/conf.py +++ b/conf.py @@ -126,7 +126,6 @@ mermaid_output_format = "raw" exclude_patterns = [ - "proposals", "_build", "src", ".venv", diff --git a/design/server-architecture.rst b/design/server-architecture.rst index e1bf0e5..e739402 100644 --- a/design/server-architecture.rst +++ b/design/server-architecture.rst @@ -42,6 +42,18 @@ The current library mutation handler discards client-supplied ``updated_at``, applies supplied fields, and assigns a server update time. Tombstones protect deleted entities. JSON payloads are not recursively merged. Owner serialization, atomic batches, and history validation remain part of the server contract. +Reading history is an immutable ledger: corrections append reversals, and progress +is projected from eligible time intervals and document coverage. Tracking writes +are staged locally until the server advertises the current schema contract. + +Timers use versioned transitions under the same owner lock. The first accepted +branch wins; stale branches are acknowledged without applying their effects. Saving +a timer, appending its activities and updating book progress form one transaction. +Conflicting saves discard the whole batch. Terminal rows prevent delayed offline +writes from reviving completed timers. Native notification actions are journaled +on the device and replayed through the client repository; they do not write to +PowerSync independently. + These mechanisms need acceptance coverage across the full set of mutable fields before the whole requirement can be marked verified. @@ -72,9 +84,7 @@ against the configured server. External provider photo URLs remain supported, but are fetched without Papyrus credentials. Downloads are private and marked ``no-store``. Replacing a photo removes the previous file after commit, and permanent account deletion removes the entire owner media directory, including -photos. Failed writes retain the previous reference and file. This increment -does not change database schemas or PowerSync projections. Deploy the updated -server before clients that offer photo uploads. +photos. Failed writes retain the previous reference and file. Account deletion and recovery ----------------------------- @@ -95,11 +105,22 @@ failures, with row locks coordinating multiple workers. Local device libraries and external provider files remain outside this cleanup. Previously issued PowerSync tokens remain usable until expiry and cannot be renewed. -Planned contracts ------------------ +Verification before sign-in +---------------------------- + +Registration creates a pending account and queues verification mail without +issuing an application session. Login, refresh and authenticated API access require +verification of the account's current primary email. Verification consumes a +single-use email-bound token; the client then signs in separately. Google sign-in +uses provider authority for eligible Gmail/Workspace identities and Papyrus email +verification for other addresses. Guest data stays separate throughout these flows. + +The generated :doc:`/api/index` defines the wire schemas. Auth services and their +regression tests own eligibility, token consumption and delivery behavior. + +Planned capabilities +-------------------- -Email verification before sign-in, recoverable account deletion followed by -permanent cleanup, and full-library content search are requirements with remaining -implementation work. They are not capabilities established by the architecture -diagram above. Additional storage providers remain committed targets, but -automatic failover is outside scope. +Full-library content search and additional storage providers remain product +targets. Their presence in requirements or diagrams does not establish implemented +behavior. Automatic storage failover is outside scope. diff --git a/proposals/authentication.rst b/proposals/authentication.rst deleted file mode 100644 index 83826a0..0000000 --- a/proposals/authentication.rst +++ /dev/null @@ -1,463 +0,0 @@ -Verification before sign-in -======================================================================== - -Status: proposed, pending review. This contributor proposal is excluded from the -published documentation. It is not a user manual or a description of the system. - -Tracking: `docs #81 `_. -Implementation: `server #22 `_ -and `client #60 `_. - -This is the proposed HTTP contract for ``TASK-AUTH-01``, implementing the -verification portions of ``FR-1_1``, ``FR-1_2`` and ``FR-1_3``. -It becomes the implementation baseline when this documentation change is merged. -It is not a description of deployed behavior. Server and client delivery remain -``TASK-AUTH-02`` and ``TASK-AUTH-03``. Password policy, password recovery -policy, account deletion policy and full-library search are outside this change. - -Source review on 2026-11-06 used server revision -``c71a82cb70a62f0f5ded5330827aa1cdebee737c`` and client revision -``3a3f6b5c1cffc33930dcfdf956ac272fbb007f1c``. Registration currently creates an -unverified user and immediately issues tokens. Login, refresh and the API session -dependency do not check verification. Verification/resend services already exist, -but registration does not send their email. The client parses registration as -``AuthTokens`` and saves those tokens immediately. The Google identity model -does not retain the hosted-domain claim needed for the authority decision below. - -Invariants and response types ------------------------------------------------------------------------- - -Examples use the default ``/v1`` API prefix. Clients must use their configured -``PapyrusApiConfig`` prefix, never hardcode it. Requests and responses use JSON. -Auth responses use ``Cache-Control: no-store``. Secrets, verification links and -OAuth codes must not appear in application logs or analytics. - -An account is eligible for application access only when its current primary -email has accepted verification evidence and the account is otherwise active. -Neither a pending account nor a verification token is an authenticated session. -No pending response contains ``access_token``, ``refresh_token``, a PowerSync -credential or a library-access capability. Verification itself never signs in. -After verification the user repeats password or Google sign-in. - -Successful login, Google exchange and refresh retain the existing ``AuthTokens`` -shape and HTTP 200. No new discriminator is required for successful tokens: - -.. code-block:: json - - { - "access_token": "", - "refresh_token": "", - "token_type": "Bearer", - "expires_in": 3600, - "user": { - "user_id": "00000000-0000-4000-8000-000000000001", - "email": "reader@example.com", - "display_name": "Reader", - "avatar_url": null, - "email_verified": true, - "created_at": "2026-11-06T10:00:00Z", - "last_login_at": "2026-11-06T10:10:00Z", - "connected_providers": [] - } - } - -The example expiry is illustrative. Configured token lifetimes remain unchanged. -Refresh rotation and session revocation remain mandatory. PowerSync credentials -remain separate short-lived tokens issued by ``POST /auth/powersync-token``. - -Registration ------------------------------------------------------------------------- - -``POST /v1/auth/register`` keeps ``RegisterRequest`` unchanged: - -.. code-block:: json - - { - "email": "reader@example.com", - "password": "", - "display_name": "Reader", - "client_type": "mobile", - "device_label": "Reader's phone" - } - -``client_type`` and ``device_label`` remain optional with existing defaults and -validation. They do not create a session or device registration at this stage. -HTTP 201 returns a new ``PendingVerificationResponse``: - -.. code-block:: json - - { - "status": "verification_required", - "email": "reader@example.com", - "message": "Check your email to verify your account, then sign in.", - "resend_after": 60 - } - -``email`` is the normalized submitted address. ``resend_after`` is a nonnegative -integer in seconds. A 201 means the pending account, password hash, verification -token and durable email-delivery job were committed together. It does not mean -SMTP delivery has completed. No ``AuthSession`` row is created. - -An existing email returns HTTP 409 ``CONFLICT`` with the existing message -``An account with this email already exists`` and empty details, whether pending -or verified. Never replace its password, name or provider links. After an -ambiguous network failure, the client offers sign-in and resend rather than -assuming registration failed. This deliberately retains the existing registration -enumeration behavior. Resend must not disclose the account's existence or state. - -Password login and refresh ------------------------------------------------------------------------- - -``POST /v1/auth/login`` keeps the existing request: - -.. code-block:: json - - {"email": "reader@example.com", "password": "", "client_type": "web"} - -Check credentials before disclosing verification state. Wrong passwords, missing -accounts and accounts without a password credential return HTTP 401 -``UNAUTHORIZED`` with ``Invalid email or password``. Valid credentials for an -active, unverified account return HTTP 403: - -.. code-block:: json - - { - "error": { - "code": "EMAIL_VERIFICATION_REQUIRED", - "message": "Verify your email address before signing in.", - "details": {"email": "reader@example.com", "resend_after": 0} - } - } - -The details contain the verified credential owner's current email and remaining -resend cooldown. Login itself does not send email. The client offers resend, -sign-in after verification, and return to the guest library. Correct credentials -for an eligible account return HTTP 200 with ``AuthTokens``. - -``POST /v1/auth/refresh`` keeps ``{"refresh_token": ""}``. -An eligible session returns HTTP 200 with rotated ``AuthTokens``. Missing, -expired, revoked or unknown credentials return HTTP 401 ``UNAUTHORIZED``. -A still-valid legacy session belonging to a now-ineligible unverified account -returns the same HTTP 403 envelope, revokes that account's application sessions -and commits the revocation. It must not rotate or issue tokens. Later retries -with a revoked token return 401. A failed response must not roll back revocation. - -Apply the same eligibility check in the shared API session dependency, including -media, sync uploads and PowerSync credential issuance, so existing API JWTs cannot -bypass the refresh check. Preserve credential validity and disabled/deletion -checks before ordinary access. Logout may revoke an otherwise valid unverified -session without granting access to other endpoints. -Session creation, refresh, verification and recovery must serialize their account -state checks with deletion using the existing owner-before-credential lock order. -No concurrent request may mint a session from verification evidence invalidated -by another committed transition. - -Resend, delivery and verification links ------------------------------------------------------------------------- - -``POST /v1/auth/resend-verification`` remains unauthenticated and accepts -``{"email": "reader@example.com"}``. HTTP 200 uses: - -.. code-block:: json - - { - "message": "If the email is registered, a verification link has been sent", - "resend_after": 60 - } - -The existing message is retained for compatibility and means accepted for -delivery, not confirmed inbox delivery. Return the same body for unknown, -verified, administratively disabled and pending addresses. Only an eligible -pending address queues email. Suppress requests within the recipient cooldown -without revealing that suppression or resetting the cooldown. - -Registration and resend share a persisted, normalized-recipient cooldown of -60 seconds, enforced across workers. Apply it to automatic Google verification -mail as well. Public attempts also have an IP limit of at most five requests per -minute per auth endpoint, including password login. A stricter configured limit -is allowed. Per-IP throttling applies before account lookup and returns HTTP 429 -``RATE_LIMIT_EXCEEDED``, ``Retry-After`` seconds, and matching -``error.details.retry_after``. Recipient suppression still returns generic 200. - -Verification tokens are opaque, hashed for lookup, bound to the user and exact -normalized primary email, single-use and valid for 24 hours from issue. Resend -does not invalidate another unexpired link, avoiding denial of service through -unsolicited resend. Successful verification consumes all outstanding verification -tokens for that address and cancels its pending verification deliveries. Changing -the primary email invalidates old evidence and tokens. Duplicate delivery of the -same email must not extend validity. - -Use a dedicated verification-delivery job or an explicitly typed extension of the -existing transactional notification queue. The existing account-deletion notices -are not verification messages and must keep their own ordering and retention. -Commit token and delivery job atomically, retry transient SMTP failures with -backoff, and expire the job no later than its token. A queued job necessarily -contains the deliverable secret, unlike the hashed lookup row. Restrict access, -exclude it from logs, and remove it after delivery, expiry, verification or -account deletion. Do not queue raw passwords. SMTP failure after commit leaves -the account pending and permits resend after the cooldown. - -If delivery or the verification landing URL is not configured, return HTTP 503 -``VERIFICATION_UNAVAILABLE`` with message -``Verification email is temporarily unavailable. Please try again later.``, empty details and -``Retry-After: 60``. For resend this server-wide check precedes account lookup -and applies equally to all addresses. Registration must not commit a new account -in this state. Queue/transaction failures also return 503 and roll back account, -token and job changes. An unknown commit outcome is handled through sign-in or -resend, never by overwriting a pending account. - -Email links target the configured app base URL at -``/verify-email?token=``. The client route is new. It must support -direct loading while signed out, expired-link guidance and token submission to -the configured API. A GET only displays a confirmation action, so mail scanners -do not consume the link. Explicit confirmation calls -``POST /v1/auth/verify-email`` with ``{"token": ""}``. -Remove the token from visible history after capture and use a no-referrer policy -on the landing page. Do not load third-party resources with the token in the URL. -Links must resolve to the same deployment as the issuing server. A mismatch must -not switch profiles or transmit the token to another configured server. - -HTTP 200 retains ``{"message": "Email verified successfully"}``. It updates only -verification evidence, without setting tokens, moving libraries, cancelling -deletion or signing in on any device. HTTP 400 uses the same public code and -message for expired, malformed, unknown, reused and wrong-address tokens: - -.. code-block:: json - - { - "error": { - "code": "VERIFICATION_TOKEN_INVALID", - "message": "This verification link is invalid or expired. Request a new link or sign in if you already verified.", - "details": {} - } - } - -Serialize consumption and account state checks so only one concurrent request -succeeds. After a lost success response, the client can try signing in or request -another link. A replay does not produce credentials or reveal the account. - -Google sign-in and exchange ------------------------------------------------------------------------- - -Keep the existing ``GET /auth/oauth/google/start?redirect_uri=...``, validated -state, redirect allowlist, ``GET /auth/oauth/google/callback`` and HTTP 302 -handoff to the platform callback with ``?code=``. Do not put email, -access tokens or verification tokens in that callback URL. Cancellation and -provider errors continue through the callback's ``error`` parameter without a -profile switch. Invalid state is rejected before any redirect or account write. - -Validate the Google ID token signature, issuer, audience and expiry. Identify the -Google account by ``sub``, never by email. Under -`Google's email-ownership guidance -`_, Gmail -addresses establish Google authority, while Workspace requires -``email_verified=true`` and a nonempty signed ``hd`` claim. An email domain string -alone is not Workspace evidence. Other addresses require Papyrus verification, -even when Google reports ``email_verified=true``. Retain the validated hosted -domain in the internal identity/evidence model, not just the current boolean. - -New users get no session in the callback. For non-authoritative addresses, create -a pending account and queue its Papyrus verification email transactionally. -When an existing linked account still needs verification, queue mail subject to -the same recipient cooldown. An already queued delivery need not be duplicated. -An existing linked subject keeps its user ID and library. Never silently replace -its primary email with a changed provider address or downgrade independent -Papyrus verification. Authority for a provider email only establishes eligibility -when that email matches the primary address being verified. For an existing -linked subject with no primary address, bind the supplied email only if it is -not owned by another account, then apply the same verification rule. A different -subject matching an existing email continues -to receive ``account_exists`` and must prove control of that account before -linking. A new Google account without an email receives callback -``error=email_required`` without an account or session. The UI explains that -an email address is required and offers email registration. -For a legacy linked account that still has no primary or provider email, preserve -its user ID and data and return the same error with guidance to contact the server -operator. Do not silently create a replacement account. Account email editing is -outside this task. - -``POST /v1/auth/exchange-code`` keeps this request: - -.. code-block:: json - - {"code": "", "client_type": "desktop", "device_label": "Laptop"} - -An eligible account receives HTTP 200 with ``AuthTokens``. An unverified account -receives HTTP 403 ``EMAIL_VERIFICATION_REQUIRED`` with the same details as login. -The code is consumed and that consumption committed for either outcome. No -session is created for the latter. Verification mail already queued by the -callback is not duplicated by exchange. If delivery is unavailable for a pending -flow, the callback returns ``error=verification_unavailable`` without creating a -new account or handoff code. An existing pending account remains available for -resend. Verified Google sign-in must still work during an SMTP outage. - -An invalid, expired, reused or wrong-purpose exchange code returns HTTP 401 -``UNAUTHORIZED`` and grants nothing. After email verification, restart Google -sign-in to obtain a fresh code. Never promote a stale pending code into a session. -The callback must bind the handoff to its eligibility outcome, and exchange must -recheck current account eligibility before granting access. Use the existing -``AuthExchangeCode.purpose`` field with ``login`` for an eligible callback and -``login_pending_verification`` for a pending callback. Exchange accepts these two -purposes only. A pending-purpose code always returns 403, even if verification -finishes before exchange, and the client then repeats Google sign-in. -Linking and account -deletion use separate code purposes and cannot use a login code as authorization. - -Errors and existing account lifecycle ------------------------------------------------------------------------- - -New application errors use the existing -``{"error": {"code": "...", "message": "...", "details": {}}}`` envelope. -Clients branch on codes and fields, not English messages. In addition to the -endpoint-specific outcomes above, schema validation retains HTTP 422 and its -existing FastAPI ``detail`` array. Rate limiting uses the existing 429 envelope. -No failure response includes application tokens. - -Disabled accounts remain forbidden. Sign-in to a recoverable deleted account -continues to return the existing ``ACCOUNT_DELETION_PENDING`` authorization after -valid credentials, without ordinary access. Preserve explicit recovery and its -deadline. Add the shared verification guard before recovery can issue a session: -for an unverified account ``/account-deletion/cancel`` returns -``EMAIL_VERIFICATION_REQUIRED`` without consuming recovery authorization or -cancelling deletion. Verification and resend may operate during the recovery -window, but never after permanent deletion or administrative disablement. -The user then signs in again and explicitly recovers. Verification alone must -not clear ``disabled_at`` or any deletion fields. This closes an alternate token -issuance path without changing the deletion policy. - -Client state and existing data ------------------------------------------------------------------------- - -Parse registration's pending result separately from ``AuthTokens``. Parse the -403 details into a pending-verification state in ``AuthApiException`` and the -provider. Keep only the server-scoped email and cooldown needed for guidance. -Do not save passwords, verification tokens or handoff codes for later sign-in. -Reject malformed responses and token responses with ``email_verified=false`` -before saving tokens or installing an account profile. - -Registration, resend, verification, cancellation and provider errors do not change -the active library. Guest records are never merged, copied or uploaded. Only a -successful eligible sign-in invokes the existing profile-switch queue. On denied -refresh, stop sync and clear credentials while retaining account-scoped local -books, media, positions and queued writes. Keep those writes in their original -profile, available for reconnection after verification. An unrelated account or -server must never acquire them. Guest access remains available. Offline cached -reading is not itself a new server session and must remain usable. - -Existing verified users keep their identity, password, library and valid sessions -when their verification evidence is trustworthy. The current boolean conflates -Papyrus verification and Google claims, so migration cannot simply trust all -true values or reset all users. Add durable evidence bound to the verified email -and source (Papyrus email link or authoritative Google identity). Backfill only -where retained records establish that evidence. Password-only accounts marked -verified by the existing email flow can retain it. For Google-linked accounts -without attributable evidence, require a fresh authoritative Google sign-in or -Papyrus link, preserving all data. Do not infer Workspace authority from a stored -domain or ``email_verified_at_provider``. Record the affected account count and -the exact migration predicate in the server PR before activation. - -Implementation map and acceptance ------------------------------------------------------------------------- - -Paths below are relative to the Papyrus workspace. They identify the current -owners and required changes, not code delivered by this document. - -.. list-table:: - :header-rows: 1 - :widths: 42 58 - - * - Owner - - Changes and focused evidence - * - ``server/papyrus/schemas/auth.py``, ``api/routes/auth.py`` - - Add pending response and explicit errors. Preserve successful tokens, - request fields and configurable prefix. Export OpenAPI after implementation. - Update ``core/exceptions.py`` and ``main.py`` for the new error codes, - ``Retry-After`` and no-store headers, including dependency failures. - * - ``server/papyrus/services/auth/{sessions,email_flows,google,_core,types}.py`` - - Enforce eligibility, retain Google authority evidence, consume handoffs, - bind verification tokens to email, serialize retries and revocation. - * - ``server/papyrus/api/deps.py``, ``services/account_deletion.py`` - - Guard existing access JWTs, PowerSync issuance and recovery without changing - deletion policy. Keep logout available for revocation. - * - ``server/papyrus/models/{auth,user}.py``, ``alembic/versions/``, - ``services/account_notifications.py``, ``services/auth/email_flows.py``, - ``core/rate_limit.py``, ``config.py`` - - Add evidence, address binding, recipient cooldown and durable verification - delivery. Preserve existing notice behavior and data. Document migration. - * - ``client/app/lib/auth/{auth_models,auth_api_client,auth_repository}.dart``, - ``providers/auth_provider.dart`` - - Parse pending/error outcomes before token persistence. Preserve refresh, - logout, account recovery, cancellation and server/profile isolation. - * - ``client/app/lib/pages/{register_page,login_page}.dart``, - ``pages/auth/{actions,oauth_callback_page}.dart``, ``config/app_router.dart`` - - Add verification guidance and cold-loadable ``/verify-email`` route, - resend cooldown, invalid-link/retry states and existing platform callbacks. - * - ``server/tests/api/routes/test_auth.py``, ``tests/services/test_auth.py``, - ``tests/api/routes/test_account_deletion.py``, ``tests/services/test_account_deletion.py`` - - Verify no session for pending accounts, old JWT rejection, refresh revocation, - all Google authority cases, recovery guard, expired/reused/concurrent tokens, - delivery failures and migration preservation using a distinct test database. - * - ``client/app/test/auth/{auth_api_client_test,auth_repository_powersync_test,auth_repository_account_deletion_test,token_store_test}.dart``, - ``test/providers/auth_provider_test.dart``, ``test/widgets/auth/``, - ``test/powersync/sync_profile_switch_queue_test.dart`` - - Prove pending outcomes never save tokens or activate profiles. Cover retries, - cancellation, reload, server changes, guest isolation and preserved offline - queues. Add focused landing-page and notification-queue regressions. - -The server task must also cover unknown/verified/pending resend responses, -cross-worker cooldowns, atomic queue failure, delayed SMTP delivery, token/email -mismatch and legitimate verified-user regression cases. Google provider tests -may use validated-claim fixtures. Real provider smoke tests remain opt-in. - -Run ``tools/papyrus check server`` and the focused auth, recovery and migration -tests against a distinct test database. Run ``tools/papyrus check client`` and -the focused auth/provider/widget tests for the client task. Database test runs -must not overlap on the same test database. - -The client task must supply narrow-mobile and desktop layout evidence plus web -compilation, with actual callback checks for the available web, desktop and -mobile targets. Record unrun devices explicitly. A browser build does not prove -native deep links. The API prefix and nondefault self-hosted app base path belong -in callback/verification-link tests. - -Rollout and rollback ------------------------------------------------------------------------- - -1. Merge this contract first. Keep the product requirements partial until their - behavior is implemented and accepted. Do not change the committed OpenAPI - snapshot to pretend these proposed responses already exist. - After merge, record acceptance for #81 and synchronize the project from the - committed docs revision before launching the dependent server task. Review - any ``spec-changed`` markers deliberately rather than bypassing task gates. -2. Implement and review the server task against this contract, including its - additive migration and data audit. Validate in staging with the client task. - Server implementation precedes client integration, but production deployment - need not follow that order. -3. Release the compatible client before enforcing the new server behavior. It - accepts existing successful ``AuthTokens`` only for verified users, as well as - new pending responses. On an old server's unverified token response it saves - nothing, attempts revocation with the returned access token and offers resend. - Deploy the web verification landing route and confirm the public app URL and - email transport before server cutover. Native users may verify in the web app - and then repeat sign-in in the native app. -4. Apply the migration and deploy the enforcing server through the normal - ``development`` to ``master`` release process. The server must enforce the - rule for every client version, without a legacy-client bypass. Older clients - can still sign in as verified users using unchanged token responses. They may - not understand pending registration and must update to use the new guidance. - Registration on an old client still creates only a pending account and sends - email. Repeating registration does not replace it. -5. Revoke ineligible legacy sessions and reject their existing API JWTs. Previously - issued PowerSync JWTs can survive until expiry (currently five minutes by - default). Deny renewal and wait the actual configured lifetime before claiming - complete removal of live sync access. Cached offline files remain local. -6. Refresh the OpenAPI snapshot from the accepted server revision and record - endpoint, mail, profile-isolation and platform evidence. Close implementation - tasks only after their own acceptance, independently of this contract PR. - -Rollback must retain verification enforcement, evidence and pending accounts. -Do not downgrade to a server that grants unverified sessions or reverse the -migration by deleting user data. If the enforcing release cannot serve auth, -temporarily return a retryable service-unavailable response for affected auth -entry points while preserving offline reading, then fix forward. Record any -temporary outage separately from successful deployment acceptance. diff --git a/proposals/index.rst b/proposals/index.rst deleted file mode 100644 index d291ada..0000000 --- a/proposals/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -Contributor proposals -===================== - -This directory holds implementation proposals and task-specific working documents. -It is excluded from the published Sphinx site, navigation, search and requirement -export. Proposals are reviewed through their linked issues and pull requests. - -The published documentation contains the user manual, product requirements and -the system's documented structure and behavior. A proposal does not establish -that a feature exists or that a requirement has been accepted as complete. -After implementation, update the relevant maintained documentation with the -resulting behavior. Keep execution plans, rollout checklists and review evidence -with the proposal or its issue instead of copying them into system design pages. - -Current proposals ------------------ - -- `Verification before sign-in `_: TASK-AUTH-01, - `docs #81 `_. From e430388ee6b5a1b5280382e911f92cb31fbf1033 Mon Sep 17 00:00:00 2001 From: Karolis Strazdas Date: Sun, 8 Nov 2026 00:33:44 +0200 Subject: [PATCH 2/2] docs: reconcile authentication contracts with current server --- .github/workflows/ci.yml | 4 +- _static/openapi.json | 923 +++++++++++++++++++++++++++++++++++- requirements/functional.rst | 11 +- 3 files changed, 919 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 924b679..8abe8b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: - uses: actions/checkout@v4 with: repository: PapyrusReader/server - # Includes the private profile-photo API from server PR #42. - ref: 235fd051e2bf72283bcc6da4ecb2d1860b6ef332 + # Includes verified sign-in and the current library sync contracts. + ref: 0108e2db00fc907335616d37147159d804cb0030 path: .server - uses: astral-sh/setup-uv@v6 with: diff --git a/_static/openapi.json b/_static/openapi.json index 371715b..b54b87d 100644 --- a/_static/openapi.json +++ b/_static/openapi.json @@ -3838,6 +3838,56 @@ "title": "EndpointKind", "type": "string" }, + "Error": { + "description": "Standard error response.", + "properties": { + "error": { + "$ref": "#/components/schemas/ErrorDetail" + } + }, + "required": [ + "error" + ], + "title": "Error", + "type": "object" + }, + "ErrorDetail": { + "description": "Error detail structure.", + "properties": { + "code": { + "examples": [ + "VALIDATION_ERROR" + ], + "title": "Code", + "type": "string" + }, + "details": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Details" + }, + "message": { + "examples": [ + "Human-readable error message" + ], + "title": "Message", + "type": "string" + } + }, + "required": [ + "code", + "message" + ], + "title": "ErrorDetail", + "type": "object" + }, "ExportAnnotationsRequest": { "description": "Request for exporting annotations.", "properties": { @@ -4905,6 +4955,37 @@ "title": "Pagination", "type": "object" }, + "PendingVerificationResponse": { + "properties": { + "email": { + "format": "email", + "title": "Email", + "type": "string" + }, + "message": { + "default": "Check your email to verify your account, then sign in.", + "title": "Message", + "type": "string" + }, + "resend_after": { + "default": 60, + "minimum": 0.0, + "title": "Resend After", + "type": "integer" + }, + "status": { + "const": "verification_required", + "default": "verification_required", + "title": "Status", + "type": "string" + } + }, + "required": [ + "email" + ], + "title": "PendingVerificationResponse", + "type": "object" + }, "PowerSyncCrudMutation": { "description": "One owned library mutation uploaded from the PowerSync queue.", "properties": { @@ -4968,6 +5049,7 @@ "bookmarks", "book_shelves", "book_tags", + "reading_timers", "reading_goals", "reading_activities", "goal_periods" @@ -5816,6 +5898,23 @@ "title": "ResendVerificationRequest", "type": "object" }, + "ResendVerificationResponse": { + "properties": { + "message": { + "default": "If the email is registered, a verification link has been sent", + "title": "Message", + "type": "string" + }, + "resend_after": { + "default": 60, + "minimum": 0.0, + "title": "Resend After", + "type": "integer" + } + }, + "title": "ResendVerificationResponse", + "type": "object" + }, "ResetPasswordRequest": { "description": "Password reset request.", "properties": { @@ -7971,13 +8070,13 @@ "name": "Papyrus Support", "url": "https://github.com/PapyrusReader/papyrus" }, - "description": "\nREST API for Papyrus - a cross-platform book management application.\n\n## Overview\n\nThe Papyrus Server handles metadata storage and synchronization for the Papyrus\ne-book reader application. It provides endpoints for:\n\n- **Authentication**: User registration, login, OAuth, and session management\n- **Books**: CRUD operations for book metadata and file references\n- **Organization**: Shelves, tags, and series management\n- **Annotations**: Highlights, notes, and bookmarks\n- **Progress**: Reading sessions and statistics\n- **Goals**: Reading goal tracking\n- **Sync**: Cross-device synchronization\n- **Storage**: File storage backend configuration\n- **Files**: File upload/download (when server is file backend)\n\n## Authentication\n\nMost endpoints require authentication via JWT Bearer token. Obtain tokens\nthrough the `/v1/auth/register`, `/v1/auth/login`, or the\nbrowser-based `/v1/auth/oauth/google/start` flow.\n\n```\nAuthorization: Bearer \n```\n\nAccess tokens expire after `60` minutes.\nUse the refresh token to obtain new access tokens via `/v1/auth/refresh`.\n\n## Rate Limiting\n\nSync and file-upload limits are enforced per authenticated user, across sessions.\nAuthentication and OPDS relay limits are enforced per client IP. Limits are configurable:\n\n| Endpoint Category | Limit |\n|-------------------|-------|\n| Authentication | 5 requests/minute |\n| OPDS relay | 100 requests/minute |\n| File uploads | 60 requests/minute |\n| Sync transactions | 300 requests/minute |\n", + "description": "\nREST API for Papyrus - a cross-platform book management application.\n\n## Overview\n\nThe Papyrus Server handles metadata storage and synchronization for the Papyrus\ne-book reader application. It provides endpoints for:\n\n- **Authentication**: User registration, login, OAuth, and session management\n- **Books**: CRUD operations for book metadata and file references\n- **Organization**: Shelves, tags, and series management\n- **Annotations**: Highlights, notes, and bookmarks\n- **Progress**: Reading sessions and statistics\n- **Goals**: Reading goal tracking\n- **Sync**: Cross-device synchronization\n- **Storage**: File storage backend configuration\n- **Files**: File upload/download (when server is file backend)\n\n## Authentication\n\nMost endpoints require authentication via JWT Bearer token. Obtain tokens\nafter email verification through `/v1/auth/login` or the\nbrowser-based `/v1/auth/oauth/google/start` flow.\n\n```\nAuthorization: Bearer \n```\n\nAccess tokens expire after `60` minutes.\nUse the refresh token to obtain new access tokens via `/v1/auth/refresh`.\n\n## Rate Limiting\n\nSync and file-upload limits are enforced per authenticated user, across sessions.\nAuthentication and OPDS relay limits are enforced per client IP. Limits are configurable:\n\n| Endpoint Category | Limit |\n|-------------------|-------|\n| Authentication | 5 requests/minute |\n| OPDS relay | 100 requests/minute |\n| File uploads | 60 requests/minute |\n| Sync transactions | 300 requests/minute |\n", "license": { "name": "AGPL-3.0", "url": "https://www.gnu.org/licenses/agpl-3.0.html" }, "title": "Papyrus Server API", - "version": "0.0.4" + "version": "0.4.0" }, "openapi": "3.1.0", "paths": { @@ -9734,6 +9833,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -9743,6 +9872,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Exchange an OAuth browser code for Papyrus tokens", @@ -9776,6 +9925,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -9785,6 +9964,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Request password reset", @@ -9815,6 +10014,56 @@ } }, "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Return the PowerSync signing JWKS", @@ -9848,6 +10097,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -9857,6 +10136,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "security": [ @@ -9895,6 +10194,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -9904,6 +10233,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "security": [ @@ -9942,20 +10291,70 @@ }, "description": "Successful Response" }, - "422": { + "400": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/Error" } } }, - "description": "Validation Error" - } - }, - "summary": "Login with email and password", - "tags": [ - "Auth" + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, + "422": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." + } + }, + "summary": "Login with email and password", + "tags": [ + "Auth" ] } }, @@ -9984,6 +10383,36 @@ "204": { "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -9993,6 +10422,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "security": [ @@ -10013,6 +10462,56 @@ "responses": { "204": { "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "security": [ @@ -10089,6 +10588,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10098,6 +10627,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Complete the Google OAuth browser callback", @@ -10132,6 +10681,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10141,6 +10720,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Start Google OAuth login flow", @@ -10163,6 +10762,56 @@ } }, "description": "Successful Response" + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "security": [ @@ -10201,6 +10850,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10210,6 +10889,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Refresh access token", @@ -10237,12 +10936,42 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AuthTokens" + "$ref": "#/components/schemas/PendingVerificationResponse" } } }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10252,6 +10981,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Register a new user account", @@ -10279,12 +11028,42 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/MessageResponse" + "$ref": "#/components/schemas/ResendVerificationResponse" } } }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10294,6 +11073,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Resend verification email", @@ -10327,6 +11126,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10336,6 +11165,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Reset password with token", @@ -10369,6 +11218,36 @@ }, "description": "Successful Response" }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid request or VERIFICATION_TOKEN_INVALID." + }, + "401": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "Invalid, expired or revoked credentials." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "EMAIL_VERIFICATION_REQUIRED includes email and resend_after seconds." + }, "422": { "content": { "application/json": { @@ -10378,6 +11257,26 @@ } }, "description": "Validation Error" + }, + "429": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "RATE_LIMIT_EXCEEDED with Retry-After and details.retry_after seconds." + }, + "503": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + }, + "description": "VERIFICATION_UNAVAILABLE with Retry-After: 60. Retry sign-in or resend after an uncertain registration outcome." } }, "summary": "Verify email address", diff --git a/requirements/functional.rst b/requirements/functional.rst index 5b1fc74..fe3de91 100644 --- a/requirements/functional.rst +++ b/requirements/functional.rst @@ -25,9 +25,9 @@ These are product commitments, not a list of released capabilities. See - Verification links are single-use, expire after 24 hours, and can be resent with rate limiting - Guest use remains available without verification and keeps its own separate library - **Implementation:** Registration and verification services exist, but - registration currently creates an authenticated session. Requiring verification - first needs coordinated changes to the server and the client registration flow. + **Implementation:** The server returns pending-verification details without + session tokens. The client shows verification guidance and keeps the guest + library separate. After verifying the email address, the user signs in. Email ownership is required because the address identifies the account and supports recovery. See `OWASP authentication guidance @@ -50,8 +50,9 @@ These are product commitments, not a list of released capabilities. See - Successful login creates a secure session token - User is redirected to their library after login - **Implementation:** Email/password login exists, but the server does not - currently enforce email verification before issuing a session. + **Implementation:** The server requires verification of the current primary + email before issuing or refreshing a session. The client routes unverified + users to verification guidance without installing an authenticated session. .. fr:: OAuth login (Google) :id: FR-1_3