Repository navigation
PRD P4-W2: the sync protocol and server authentication - #46
Merged
Merged
Conversation
The implementer session failed with "Free usage limit reached" part-way through this task. This commit preserves what was on disk in its worktree so nothing is lost while the work is finished by hand. It is NOT a completed task: the acceptance has not been met, the gate has not been run, and no PR should be opened from this branch until it is.
…4-A02] The protocol DR-02 chose, built out of the spike rather than beside it. A device writes through the repository and records the envelope in the outbox inside the same transaction, so a row and the change that carries it commit together or not at all; a server folds each pushed envelope into the canonical tables in a transaction of its own and appends it to the change log. Versions are hybrid logical clocks in a fixed-width `wall:counter:device` form, so they sort as strings, and every envelope carries the fields it touched with the clock each of them had before the write, which is what lets a field be merged without inventing a per-field row. The three policies of spec 06 §3.2 are the merge, and they are pure functions: append-only for financial and attendance events, where a second write is a conflict and never an overwrite; cell-level merge for marks, keyed on the assessment, enrolment, learning area and component, so two devices that marked the same cell fold into one row instead of two; and field-level last-writer-wins for profiles, which keeps the losing value and records it. Nothing is dropped silently: every conflict the merge settles, every cell identity it folds, and every change a schema, guard, unique or scope check refuses lands in the conflict log as a row with both sides, and a refusal that cannot be settled is recorded as `unresolved` and shown in settings. A replica is scoped to one institution, optionally narrowed to a set of entities that is closed over the reference graph before it is used. The server filters the change log by institution and entity in SQL and re-checks each row against the scope, and the device refuses a pulled row outside its own scope as well, so a transport that hands over the wrong institution cannot put it in the database. `LANServer` opens the server node, seeds the change log from the rows it already held, and serves `/api/sync/changes`, `/api/sync/push` and `/api/sync/conflicts`; `createHttpSyncTransport` is the device's client for them. The tests run the policies against real SQLite databases: two devices editing offline, each policy proved by its own concurrent edit, a replay that writes nothing twice, a refusal that is logged rather than retried forever, a pull that refreshes live queries through the repository bus, and two institutions on one server where a scoped replica never sees the other's rows. Server-allocated commit sequences are P4-A03: a pulled row's audit event takes its number from `MAX(commit_sequence) + 1` because applying a remote row does not advance the repository's counter.
…C01] Replace the trusted `x-actor` header with real identity: a bearer session whose SHA-256 is all the database holds, so a stolen file yields no live session. - First run creates the administrator; there are no default credentials, and the endpoint refuses with `setup_complete` afterwards. - PBKDF2-SHA256 at 600k iterations, minimum ten characters, and a fixed dummy verification so an unknown username costs what a known one costs. - Lockout per username: five failures inside fifteen minutes, then locks of 15/30/60/120/240 minutes, cleared only by a successful sign-in. - Sessions: opaque `sp1_` tokens, a 30-minute idle window that slides on use, a twelve-hour absolute life, and per-device revocation. - TOTP for accounts that enrol one: RFC 6238, a replayed step refused, the secret stored AES-256-GCM encrypted under the hub's own key file. - Guardian sign-in by phone OTP; the identity it creates can never be used with a password. - Password reset that answers the same whether or not the account exists, is single use, and revokes every session of the account. - Delivery is a seam: a hub without a channel refuses with `delivery_unavailable` before it looks anything up, so the endpoints cannot probe for accounts. Authorisation is not here: any live session may still call any route, so the repository API is as open as it was. P4-C02 owns the RBAC and scope model.
…P4-W2] The hub authenticates every route but /health and the sign-in endpoints, so this suite - written before authentication existed - got 401 from every call. It now gives the seeded user a password the hub can verify (the fixture hash is a placeholder and the hub has no administrator of its own), signs in, and carries the bearer token in its request helper and in the device transport's headers. Nine of ten tests pass. The last one still fails: the transport's headers option does not reach the request its pull path makes, which is a finding about src/data/sync/httpTransport.ts rather than about this suite.
The previous commit's replacement string went through a shell that interpolated the template literal, so the transport was handed an empty bearer and every pull was refused. The token is carried as written now: the suite passes 10/10. The transport itself was never at fault - it spreads the configured headers for both push and pull.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
P4-W2: the sync protocol and server authentication. Two tasks, each verified against its branch rather than its report, and integrated here.
P4-A02 — the sync protocol
src/data/sync/**— the change log, the HLC, scoped replicas, the envelope, the merge policies and the HTTP transport — withserver/sync/**serving it and the conflict-log UI in settings (SyncConflictLog,SyncConflictsPanel,useSyncConflicts). The three policies spec 06 §3.2 asks for are named, tested policies rather than a default: append-only for financial and attendance events, cell-level merge for marks, field-level LWW with a conflict log for profiles. A conflict no policy resolves is logged and surfaced, never dropped.P4-C01 — server authentication
server/auth/**— sessions, lockout, password reset, TOTP for finance and admin, guardian phone OTP — and thex-actorplaceholder replaced by an authenticated actor, so repository writes are attributed to a real user instead of a null actor. Every route but/healthand the sign-in endpoints now requires a bearer session.What the integration exposed
These two tasks were correct alone and disagreed when combined, and the wave gate is what caught it: C01 made every route require a session, while A02's sync suite (written before authentication existed) called
/api/sync/*with no token — 10 failures, including one asserting400where the auth refusal now returns401first.The fix is in this PR and it took three parts to get right, all of which are recorded because they cost the most to find:
usertable — so the suite gives that user a verifiable password before signing in;repository.updaterequires its fourth argument ({ expectedVersion, actor, reason }), or it throwsCannot read properties of undefined (reading 'actor');headers.One correction on the record: an intermediate commit message here claims the transport had a defect, because its configured headers did not reach the pull request. That was wrong — it was a bug in my own edit, a template literal a shell had interpolated into an empty bearer.
httpTransport.tsspreads the configured headers for both push and pull, and the final commit says so.Verified
Not in this PR
P4-D01 (backups) is committed on its own branch and is not merged here: its
backupRoutes.test.tshas the same authentication gap, and merging it would put a red suite into this wave. It follows once its suite signs in the same way — the recipe is indocs/prd/workflow-log/WAVE-MAP.md. P4-D04 (hardening) waits for W3's other tasks.