| D-M4-09-0 |
2026-10-15 |
Base int/m1-wave24 at 2a72cca, branch feat/M4-09-auth-dpop in its own worktree; no local merge, no push and no pull request from this build, the PR lane lands it. No contract change of main is ported: the openapi.yaml of this base has every operation and schema the flows use, and the rulings the brief cites (D-W-100, D-W-114, D-W-115) and the rules R-CL-04, R-CL-05, R-CL-07, R-SY-01 and R-SY-14 are present. The brief names the English chapter files of main; on this base they are 08-identitaet.md, 15-client-architektur.md and 04-sync-protokoll.md, and text this ticket adds there is English. |
Instructions of the launcher, which override the brief's landing step; merged names win over the brief (D-W-100, D-W-114). |
| D-M4-09-1 |
2026-10-15 |
JOSE is built by hand on crypto and base64url (lib/src/jose.dart): compact JWS, JSON without whitespace, DPoP header members in the order typ, alg, jwk, ES256 signatures as R \|\| S with 32 bytes each from the signer port (RFC 7518 3.4), the RFC 7638 thumbprint over crv, kty, x, y. htu drops query and fragment and the default port and lowercases scheme and host; the gateway form maps wss to https and ws to http. |
Brief Design note 2: a JOSE library would add algorithms and header forms the server's profile of ADR 0067 rejects. |
| D-M4-09-2 |
2026-10-15 |
NonceStore keeps the newest nonce of the server, taken from every DPoP-Nonce header (success and error) and from hello.dpop_nonce. DpopInterceptor repeats a request once on 401 use_dpop_nonce with a fresh proof; a second use_dpop_nonce goes to the caller. |
Brief Design note 3, RFC 9449 8. |
| D-M4-09-3 |
2026-10-15 |
Persist before publish: a rotated refresh token is written to the SecretStore and the write awaited before the new access token is published; refreshes of one family run as a single flight (RefreshCoordinator), and a request that failed with an older token repeats with the current one instead of refreshing again. 401 session_revoked signs out with sessionRevoked, unless the request or the refresh was sent in an earlier session (before a logout and a new login), whose late answer must not end its successor, clear the store or emit an event, and is never resent with the successor's token; a refresh rejected with unauthenticated, token_expired or invalid_dpop_proof signs out with refreshRejected; a network or other error keeps the state and emits RefreshFailed. |
Brief Design note 4: a crash between rotation and storage falls into the server's auth.refresh_reuse_grace (08 7.4) instead of a lost token; two refreshes of one family would look like theft. |
| D-M4-09-4 |
2026-10-15 |
LoginService returns LoginOutcome (LoginSignedIn, LoginPending, LoginNeedsSecondFactor, LoginFailed with the problem code) and never throws for a server answer; beginPairing returns a PairingTicket. loginWithPassword sends password and TOTP in one request because PasswordLoginRequest carries totp_code; without a code it returns LoginNeedsSecondFactor and sends nothing. The API names the device keys public_jwk and step_up_key.public_jwk (the brief's dpop_jwk and step_up_jwk). Polling of completePairing and getDevice waits client.outbox_retry_base doubled per attempt up to client.outbox_retry_cap from AppConfig; a network error, 429, a 5xx and a refresh that fails for such a reason keep polling, a 202 of completePairing means not yet approved. |
Brief Design note 5 and fixed requirement 9 (no new key). |
| D-M4-09-5 |
2026-10-15 |
StepUpInterceptor runs the step-up once on 401 step_up_required (any reason) and repeats the original request with the same Idempotency-Key and body, a new proof and the current token; a second step_up_required after a confirmation goes to the caller, a cancelled prompt or OS confirmation ends with StepUpCancelled. The step-up JWS has only {"alg":"ES256"} in its header and challenge, sid and iat in its payload. The prompt text comes from RequestOptions.extra[stepUpPurposeExtra]. Every auth repeat (nonce, refresh, step-up) sends a FormData body as a fresh clone, because Dio finalizes a FormData once; a stream body cannot be read twice, so its first error goes back to the caller. |
Brief Design note 6, 08 6.2, 6.3 and 14.3: the same key lets the server deduplicate, and no loop can ask the person twice. |
| D-M4-09-6 |
2026-10-15 |
pointycastle 4.0.0 (dev dependency of packages/auth, only the software P-256 signer of test/support/) gets a license override MIT in osv-scanner.toml, like the other pub packages deps.dev has no license for; its LICENSE file is the MIT text of the Legion of the Bouncy Castle. quality/licenses/allowed.txt stays unchanged. |
make licenses reported it as UNKNOWN; the brief names the dependency, and the override records the checked license instead of widening the allow list. |
| D-M4-09-7 |
2026-10-17 |
The ticket was ported from its integration base onto main with only its own commits; the English docs of main win (the interceptor order and signed-out bullets sit in 15-client-architecture.md 8, the package README points to the English chapter), and the section stands before M7-24 instead of at the end of this log. |
Port onto main (PR lane); other open pull requests append at the end. |