| D-M2-41-1 |
2026-10-15 |
A pairing code is stored as HMAC-SHA-256 with the pepper of auth.token_pepper_ref over the full code string, the same derivation as api_tokens.token_hash; identity.Tokens.HashHostPairingCode parses form, checksum and kind hpc first and returns identity.ErrTokenMalformed before any HMAC. |
One derivation for every token of ADR 0061; the prefix bothy_hpc_ already separates the kinds, so no extra label is hashed. |
| D-M2-41-2 |
2026-10-15 |
One issuer for createHost, issueHostPairingCode and the fallback of confirmHost: app.Service.issueCode over PairingCodes.Issue (bound to identity.Tokens.NewHostPairingCode). Case a of the brief: M2-39 already minted hpc codes through domain/apitoken and the identity port, so its names stay. |
The format and the hash derivation exist once. |
| D-M2-41-3 |
2026-10-15 |
pairHost runs its write as the rights change host.pair (with the people of the valid assignments), although 05 gives host.pair Permissions no. |
03 counts reported_session_users as rights state (D-M2-40-9), so the rights version rises first. |
| D-M2-41-4 |
2026-10-15 |
A redeemed code leaves pairing_code_hash and pairing_expires_at NULL. |
The index on pairing_expires_at serves only pairing, and a used code must never match again. |
| D-M2-41-5 |
2026-10-15 |
The key and report rules of pairHost with their errors[].code values: invalid_key (Noise key not strict base64url of 32 bytes or a low-order point; JWK kty, crv or x wrong), unknown_member (any JWK member besides kty, crv, x), empty, duplicate, too_long, pattern_mismatch, limit_reached, invalid_format, the textcheck rules for labels; every finding is returned, in body order. |
The host service is untrusted input, and a host without session users can never give a terminal (HD1). |
| D-M2-41-6 |
2026-10-15 |
Only a well-formed prefix (upper-cased, exactly terminal.fingerprint_confirm_chars characters of the Crockford alphabet) counts as an attempt; I, L, O and U are not mapped. |
A typo of the form must not burn an attempt; the client may normalise before sending. |
| D-M2-41-7 |
2026-10-15 |
409 fingerprint_mismatch of confirmHost keeps its effect under an Idempotency-Key: the handler calls idempotency.KeepEffectOnFinal, and the stage stores the answer with Store.Complete in the same unit and commits; only this answer is ever marked, a marked 401 or 5xx still rolls back. The new problem member attempts_remaining is carried by errs.FingerprintMismatch through the existing errs.ProblemExtender. |
Every failed attempt must count also under a key (TM-T19); screen A8 (M4-37) shows the remaining attempts and no other field carries them. |
| D-M2-41-8 |
2026-10-15 |
confirmHost compares the prefix before anything of the body that needs the database (the people of session_users, the credential holder, the assignment rows); it writes one confirm audit entry that also carries the declaration, plus one session_user entry per assignment, all with the same correlation. A replay with the same key gets the stored answer and never burns a second attempt. |
A wrong host must never reach an assignment or a declaration. |
| D-M2-41-9 |
2026-10-15 |
The fallback after terminal.fingerprint_max_attempts failures: state pairing, the three key columns NULL, key_fingerprint kept, pairing_code_hash the HMAC of a fresh code that is never shown with pairing_expires_at now, fingerprint_failures 0, the next version, host.updated with key_changed. |
03 demands a hash exactly in pairing, and no code may work until the owner issues one. |
| D-M2-41-10 |
2026-10-15 |
Dropping keys keeps key_fingerprint; Host and host.updated omit key_fingerprint and noise_public_key while the host has no keys; pairHost with the retained fingerprint is 409 conflict host_key_reused, with the login key of another host host_key_in_use. |
Refuses a re-sent key pair (T-TM-05). Honest limit: the server keeps no dropped key, so a host service that keeps one of its two keys is not detected; M2-42 generates both anew. |
| D-M2-41-11 |
2026-10-15 |
The visibility of host adds: any principal when the requirement of the action is exactly Public (only host.pair). |
The pairing code is the proof, checked by the use case before authorize, as for the refresh token. |
| D-M2-41-12 |
2026-10-15 |
key_changed is set exactly when stored keys were dropped (re-pairing from confirming, active or disabled, and the fallback); key_generation rises only at pairHost. Audit details: pairing_code with from_state, to_state, old_fingerprint, key_generation, sessions_ended, reason; confirm with result, fingerprint, key_generation, attempts_remaining, llm_credentials, credential_holder_id, declared_server_host, reason; host.pair with fingerprint, old_fingerprint, key_generation, platform. Fingerprints as compact lower-case codes; the host is the resource, not a detail; from_state and to_state follow M2-39 instead of the brief's state_before and state_after. |
Devices pin keys by generation (24 5.5); one vocabulary for state changes in the audit. |
| D-M2-41-13 |
2026-10-15 |
New keys terminal.report_label_max_chars (default 100) and terminal.report_max_entries (default 64) of chapter 24; the start check fingerprint_keys requires terminal.fingerprint_confirm_chars between 1 and the length of the compact fingerprint and terminal.fingerprint_max_attempts at least 1. |
Limits only through named keys; M2-43 reuses the report keys for report. |
| D-M2-41-14 |
2026-10-15 |
The job terminal.sweep (and its clause on expired codes) is not built here; an expired code is rejected at redemption, and its hash stays while the host is pairing. |
No ticket row names the job; follow-up. |
| D-M2-41-15 |
2026-10-15 |
Names that differ from the brief: PairingCodes.Issue instead of New; the port methods take actor.Actor and commands carry terminal.Precondition (D-M2-39-16); RepairEffects(state, disabledReason); field errors are errs.FieldError; Cache-Control: no-store comes from the chain for every API answer, not from the handler; issueHostPairingCode declares Idempotency-Key in the contract and marks its answer as a one-time secret like createHost instead of x-idempotency-exempt; TestPairingRoutes_AZP1 is part of TestAZP1_HostRoutes; StorePairing writes reported_session_users with the encoder of the report of M2-40 in the pairing statement instead of calling ApplySessionUserReport, which would write a second version and event; the instance id comes from instance.State.Load; the test world makes Alex an owner by a fixture row; 08 section 12 already named the kind hpc. |
The merged code of M1, M2-39 and M2-40 wins over the brief (Boundaries). |
| D-M2-41-16 |
2026-10-15 |
The spec chapters on this base are German, so the changes to 02, 06 and 24 are written in German like those of M2-40; the planning rulings D-W-65, D-W-101 and D-W-102 of main change no contract and are not ported. Working directory labels are part of Host.reported_session_users, so they appear in the hosts row and in host.updated; TestPairingNeverLeaksSecrets checks the audit, problem bodies and logs for them. |
Follow the language of the chapter; the brief's leak list would contradict the Host schema of M2-39. |
| D-M2-41-17 |
2026-10-19 |
Port onto main from the integration squash of M2-41 on top of M2-40: the spec additions to 02, 06 and 24, the threat model row and ADR 0061 are written into the English documents of main (this supersedes the language part of D-M2-41-16), the terminal README and the requirement label are English, and the generated files (REST, routes, SDKs, configuration reference, config schema) are regenerated. |
main is English; the integration branch still carries the German chapters. |