oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

Existing-account link storage integration

Implementation subtask of approved Docs46 design2e69825, on reviewed preview8a12672. No HTTP routes, UI, configuration, authentication adapter, new principals or deployment. The integrated branch still needs independent Fable review and the full Docs gate.

internal/docs/linkstore exports:

  • Schema: additive subsystem migrations; merge after the existing Docs Schema in BootSchema. It references people but does not change people, documents, grants or content encodings.
  • New(db *sql.DB, dek []byte, origin, issuer string) (*Store, error): exact configured HTTPS origins and a dedicated 32-byte storage/handoff DEK. Public shared seal package, namespace oficina/docs; never the content key. After migration/construction, call Sweep(ctx, now) at boot.
  • Start(ctx, Local, returnPath, now) (initSecret, error): only after explicit same-origin POST and caller's method-aware local-auth predicate. Invalid navigation defaults to / through shared handoff.SafePath. Shared 10,000-row initiation/pending cap, swept on new starts.
  • BindNonce(ctx, initSecret, Local, nonce, now) error: shared consumer start hook, once, before redirect/cookie. Uses the shared ValidNonce format.
  • Complete(ctx, initSecret, Local, Home, now) (pendingSecret, error): only after shared consumer has verified and consumed the assertion. Compares exact issuer, subject validity, nonce, current local person/session/auth time, and actual Home freshness. Never replaces local session.
  • Pending(ctx, pendingSecret, Local, now) (Pending, error): confirmation continuation read, without installing a mapping.
  • Confirm(ctx, pendingSecret, Local, now) (Pending, error): explicit same-origin confirmation POST; pending deletion and mapping write commit together.
  • Resolve(ctx, issuer, subject) (personID, currentAddress, error): joins the exact configured issuer/subject mapping to the current existing person. No EnsurePerson or sample seeding.

Local has PersonID, SessionToken and AuthTime. The caller derives these from the current independently verified local session, with the approved method-aware predicate: magiclink/keymail only, zero-time fallback only for actual local magiclink's stored At. SessionToken is the actual resolved session cookie, not Subject; this package hashes it immediately for storage. Repeating the same person under a different session or different auth time fails. Never construct Local from Home claims.

Home contains already verified Issuer, Subject, Nonce and original AuthTime; never raw assertions or request parameters. Pending returns PersonID, ReturnPath and HomeAuthTime to server code. Both returned secrets need distinct host-only Secure/HttpOnly/Lax cookies and strict duplicate/missing-cookie handling in the caller. No proof, assertion or return path belongs in an unsealed cookie/query string.

Initiation and pending JSON payloads are sealed to the stage, random-secret hash and exact local origin. The encrypted payload repeats its expiry and configured issuer. Raw session/cookie/nonce values, pending Home identity and resource paths do not appear in DB/WAL; permanent confirmed identity mappings intentionally contain issuer/subject/person as required by the design. Proof lifetimes end at the earliest initiation, local-auth or Home-auth five-minute deadline; the exact expiry instant is refused.

The transaction takes a write lock before any reads or capacity checks. A persisted clock high-water prevents a restart or clock adjustment from reviving stale authentication. Within the shared consumer's 30-second backward tolerance, nonfuture checks use wall time while age and expiry use persisted high-water. Larger backward changes refuse before state access. This is a fail-closed implementation of dual freshness, not authority to refresh either authentication time. Clock/environment refusal retains state until recovery/expiry; it does not grant a link.

A validly addressed row rejected for binding, freshness, tampering or mapping conflict is deleted and committed; it cannot become reusable if the conflicting mapping later disappears. Separate valid proofs for an already identical mapping are idempotent. Replaying the same cookie fails. Storage failures roll back both deletion and mapping, returning only a fixed storage error, so recovery may retry without a half-written mapping. No unlink/reassignment API exists.

Validation: package race tests and vet pass. Tests cover two identities/persons, unchanged legacy rows, current address lookup, nonce/local session binding, freshness, invalid paths/cookies, sealed DB/WAL state, wrong origin/key/row/ciphertext/expiry, proof replay, concurrent unique links and 10,000-row admission, clock rollback, and injected storage failure. Meaningful Go overlay mutations in /tmp/oficina-docs-link-store-mutations fail for accepted cross-session proofs, accepted unbound/wrong nonce proofs, and reusable rejected conflict proofs. The root integrator owns whole-branch Fable review, full gate and real browser acceptance.