oficina / home Public

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

Plain git — no account needed to clone.

Download

Download this file

1# Home and team app entry
2
3Status: bounded Home/directory transport design approved by Claude Fable 5.1 at 36f876c on 2026-09-08; code still requires review. The user has prioritized docs.oficina.works → Home sign-in → team choice → team Docs, and accepted the existing team-subdomain shape. This branch starts with the shared Home/directory exchange; the acceptance target still includes Docs and two teams. Copy review is waived for this round. Fable reviews each implementation stage.
4
5## Deployment and authority
6
7The credential origin remains id.oficina.works and the directory remains oficina.works. Team instances use <team>.docs.oficina.works. The common docs host becomes an entry point only after the existing production Docs instance has a verified team alias and all existing links still resolve. No replacement of the production database or keys is part of this work.
8
9The Home binary runs in two explicitly configured roles, credential and directory, with separate processes, databases and host-only session cookies. Each role refuses the other origin's routes even when a caller supplies its Host header. The credential role has the assertion private key; the directory has only pinned public verification keys. Existing sign-in secrets stay with their existing databases. The directory never obtains a credential session, and the credential origin never renders a team label or account-authored string.
10
11For the first isolated pilot, the operator supplies a bounded immutable consumer manifest through deployment configuration. Each consumer has an opaque ID, exact canonical HTTPS origin and fixed callback path /auth/assertion. No request can register or alter a consumer. The directory is a consumer too. Consumer registration in the eventual production provisioning/drain path remains explicit; pilot bootstrap is not a web administration endpoint.
12
13The new rastrillo assertion package signs and verifies the opaque person_ref, exact origins, independent jti and consumer nonce, issued/expiry/authentication time. P-256 reuses crypto.MarshalKeypair for the credential-only signing secret. Max lifetime is 120 seconds. Verification keys are pinned for this first deployment; no token-driven key fetch. A rotation deploys old+new public keys before switching the signer, then removes the old key after the handoff horizon. No email, role, membership, team authority or resource path rides in an assertion.
14
15## Browser exchange
16
171. A consumer's GET /auth/start selects a validated local return path (bounded, no scheme/authority/control characters/backslashes, no recursive auth routes). It generates independent random browser secret and signed nonce. Store only their hashes with purpose, local return path and ten-minute expiry in the consumer database. Every local return path is sealed before storage. The maximum path+query length is 2048 bytes; a query string is permitted for existing ?team= navigation. The __Host- pending cookie is Secure, HttpOnly, Path=/, SameSite=Lax and has no Domain. Duplicate cookies are refused. One pending request per browser is the initial bound: starting another in the same consumer replaces the previous pending request. Different team origins remain independent.
182. Redirect to the configured credential origin /authorize with only registered consumer ID and nonce. These are request identifiers, not signed assertions or identity. Strict query parsing rejects duplicates and unknown keys. The credential origin validates the registered consumer and nonce before doing anything. It retains the authorization request in a sealed, ten-minute __Host- intent cookie. No arbitrary return URL is accepted. Signing in continues to the fixed /authorize/resume route through the existing sign-in return machinery, including second factor and recovery.
193. Only a complete credential session may mint. The original AuthTime (or At when absent) is retained. The issuer makes a fresh jti and a 120-second assertion for exactly that consumer origin and nonce. A handoff page posts it to the registered callback. The page contains no account-authored visible string, uses no-store and strict-origin-when-cross-origin, and uses the process-wide CSP with form-action self plus every exact manifest origin (including the directory). The handoff URL carries no identity or signed token. Never use no-referrer here: browsers may send Origin: null on the cross-origin POST.
204. The automatic handoff needs one narrowly allowed static same-origin script that submits this form; it has a visible Continue button when JavaScript is off. This is an implementation decision under the user's explicit approval of the automatic-return flow in this session, not an attribution to the older passkey-only ruling. Record it as that current authorization when amending the spec and the exact template/path script allowlist. All other credential pages keep their existing script restrictions. No inline script or arbitrary form target is allowed.
215. A callback accepts only POST application/x-www-form-urlencoded with a bounded 12 KiB body, exactly one assertion field and the exact configured issuer Origin. Mount this route outside ordinary same-origin CSRF middleware only; all other mutations retain that middleware. Reject a foreign Origin even when its Sec-Fetch-Site says same-site. No CORS is added.
226. SameSite=Lax cookies may be absent on a cross-site POST, so the callback must not depend on receiving them. Verify the assertion, write it sealed into a two-minute consumer inbox row keyed by the hash of a fresh random 32-byte handle, set that handle in a __Host- handoff cookie with Secure, HttpOnly, Path=/, SameSite=Lax and no Domain, and 303 to the consumer's fixed GET /auth/finish. No session is minted yet. Prune expired inbox/pending rows on each write and boot; cap each table at 10,000 live rows. Evict the oldest pending row to admit a new start when full; this discards only unauthenticated navigation intent and requires that browser to restart. Refuse callbacks if the inbox is full. Tokens stay in sealed storage, never in a cookie large enough to hit browser size limits. Inbox has a unique (issuer, jti-hash) index, so replaying a callback cannot allocate another row. That top-level GET carries the consumer's pending cookie. Read and clear the handoff cookie, load its sealed inbox row by handle hash, reverify the assertion against the current clock, compare the signed nonce with the pending request using constant-time equality, and compare the browser secret hash. An attacker may overwrite a handoff cookie only to cause a failed login: a different browser's pending state never matches.
237. Atomically delete the live pending request and its inbox row and insert (issuer, jti, expiry) into the replay table before issuing a session. Duplicate/concurrent submission has one winner. On a storage failure, refuse. A later session-write failure consumes the assertion; the user starts again. Never roll back replay consumption after a session cookie may have been sent. Existing identity is not silently switched by a handoff: an already signed-in consumer with a different subject refuses and requires explicit sign-out first.
248. Replay pruning requires a durable clock fence. Track the maximum observed wall time; accepted expiry remains on each replay row in the same transaction. Refuse exchanges after clock rollback more than 30 seconds below that wall-time fence; do not prune or mint while fenced. Retain replay rows through expiry plus 30 seconds so a tolerated backward clock adjustment cannot resurrect a pruned token. On normal operation, delete entries whose expiry plus 30 seconds precedes the stored wall-time fence, after advancing that fence transactionally. All comparisons are overflow-safe. A process restart uses the stored fence. A restored consumer database requires draining and an explicit reset of assertion trust/pending state before accepting new traffic; deployment must not claim arbitrary snapshot rollback is replay safe.
259. Issue a separate consumer session with the verified subject and original AuthTime, then redirect to the stored local path. The directory session proves identity only. Every team choice and app request still needs membership or an existing individual grant.
26
27## Directory and Docs stages
28
292026-09-11 implementation extension: the reviewed Docs chooser also serves Sheets, Slides, Calendar, Meet and Memoria through the same membership query, canonical app-origin validation and continuation template. All six remain provisionable false. This adds no public gateway or live team provisioning; the existing-instance, deep-link and credential-preservation conditions below still apply.
30
31The directory owns teams, authoritative memberships and team instance routes. Choose only among the signed-in person's active memberships that have the requested app provisioned; one team redirects automatically, several render a shared Estilo choice page, none render an empty state. Only server-owned origins enter redirect targets. POST choice checks current membership again. Persisting a last choice may change the default, never access.
32
33Call 8 uses a consumer token scoped to one team. A complete roster includes generation and epoch and is paged with epoch-bound cursors. Apps stage all pages and publish one complete roster atomically, including an explicitly complete empty roster. Stale or failed refresh never extends the maximum age. Every request, including an existing session, loses team-standing access when the roster becomes stale; existing individual grants remain their own authority. A restored Home generation requires directory-owner confirmation before consumers trust it. Roster timing and recovery tests precede its deployment.
34
35Existing Docs people are keyed by verified email, while Home uses person_ref. Add a stable (issuer, subject) → local PersonID mapping. Link an existing person only after proving both an existing Docs sign-in and the Home identity in one browser-bound transaction. Never infer a mapping by email from an assertion, generate a fake email, or replace document ownership IDs. New Home-only people need a nullable legacy-address field or separate identity model and a forward migration preserving the existing unique addresses. Existing address grants continue to resolve through a linked verified legacy identity. Home membership itself grants no access to existing private documents.
36
37The common app host has no authority over team content. Its GET routes initiate directory selection and preserve bounded navigation intent. Existing production document links must continue to resolve through the original team's verified alias; a deployment must not redirect every old link to whichever team the user most recently selected. Exact behavior for this compatibility entry is reviewed before the common host changes role.
38
39## Release evidence
40
41First exercise the exchange between isolated credential/directory origins with real HTTPS, Chromium and WebKit, JavaScript on/off. Test foreign origin, missing/duplicate cookies, wrong nonce, wrong audience, expired/reused tokens, parallel callbacks, stale auth_time, second-factor continuation, clock rollback/restart and no token in URLs/logs. Existing Home privacy and script gates are extended rather than replaced.
42
43Then run two isolated Docs teams: one-team automatic choice, multiple-team selection, direct resource return, separate tabs for each team, membership removal and failed roster refresh, account linking preserving existing ownership and grants, sign-out and session expiry. Full app CI, Fable review and verified canary versions precede production. Notify the user after every verified deployment with the exact URL and what to try; a library merge is not a deployed sign-in flow.
44
45## Review corrections and implementation choices
46
47Fable reviewed d116692 and found a functional referrer-policy bug, missing explicit cookie/CSP details and write amplification. The revised protocol above fixes each. Two-process topology is the already deployed preview shape, now made explicit and enforced by role; each database needs its own privacy and restore checks. The directory loader requires only its sealing key and pinned verification keys, never credential mail, pepper or signing material. Pilot immutable registration stays scoped to isolated canaries; it is not a completed production provisioning design. Oldest-pending eviction is the chosen bounded-cache policy.
48
49Update the Home spec in the implementation commit: §4/§10 describe the explicit role topology; §5.3 records P-256, signed nonce/auth_time and pinned pilot keys; claims omit optional prefs and roster_epoch in this bounded stage rather than falsely claiming those consumers are implemented. Preserve their later contracts. Amend script ruling 21 with the current user's approved automatic-return requirement and the narrowly chosen static handoff enhancement; label the implementation decision honestly, not as a verbatim historical ruling. Extend scriptAllowed with exactly the handoff template and static path, credentialReturn with /authorize/resume, and existing privacy/log gates with sealed inbox/pending, replay, directory session and known assertion-byte canaries.
50
51Reuse the consumer exchange across Home and Docs rather than implementing two subtly different callbacks. The framework's pure core approval does not approve this adapter: its storage/HTTP implementation will have its own tests and review. A shared consumer accepts the application's database handle, sealer and session-issuance callback; it does not hold memberships or item grants. Keep legacy app authentication working while adding opaque Home subject mapping; do not require a full framework/UI migration merely to import the pure assertion primitive.
52
53
54Shared consumer implementation is a nested module at handoff/, module amadan.net/oficina/home/handoff, with a minimal dependency graph. The actual hub now resolves go-import for both amadan.net/oficina/home and amadan.net/rastrillo/rastrillo: go list -m successfully fetched Home36f876c and Rastrillo29e4491 on 2026-09-08. This corrects the review's stale discovery assumption. A nested module remains necessary to avoid importing the Home binary's unrelated dependency requirements. Verify remote fetching of the nested module from a frozen pushed commit before any app pins it.
55
56The pure core29e4491 passed full local and actual CI and Fable whole-code/docs review, and merged on Rastrillo main997935b; mirror fast-forward verified. The standalone consumer adds sealed pending/inbox/replay storage and HTTP handlers with tests. Its source3c550c3 is approved and remotely fetchable; the Home mounting work consumes that exact version. Before mounting, amend the Home spec, credential script allowlist and privacy gates as listed above. No deployment is claimed for these library changes.
57
58Home mounting now adds explicit role loaders, issuer intent and signed handoff, directory session callbacks, narrow script allowance and manifest CSP. Integration tests cover both databases, opaque subject, preserved authentication time, fresh email continuation and recovery second factor. Deployment and independent Home code review remain pending.
59