Signed-off-by: Cole Speelman <crspeelman@gmail.com>
8.4 KiB
Web Foundations dogfood notes
This living note records concrete pressure discovered while Gamertan services adopt Web Foundations. It is implementation evidence, not a promise that every application concern belongs in the shared module.
Gamertan accounts and commerce
-
Customer profile and membership editing requires current ownership for every write, not just changes involving another owner. The existing generic methods intentionally permit application-authorized delegated administrators, so an application preflight alone would leave a demotion race. Explicit owner-managed methods now share their transactional cores while rechecking current direct ownership before any write. Tests cover stale authority and optimistic state, last-owner protection, concurrent winners, and audit-failure rollback. No extra passkey ceremony or database migration is needed for this invariant.
-
A customer may need both purchasing and billing access. Replacing one role at a time would create partial permission states and misleading audit history. The role-set extension commits all direct roles together with optimistic binding IDs and current owner authority. Multiple-role invitations carry the same combination atomically, with stored owner-managed policy rechecked when accepted. SQLite tests cover concurrent winners, write rollback, demoted or removed grantors, and attempted implicit reactivation of suspended members. This adds schema 10; application vocabulary, allowed roles, invitation delivery, ordinary-customer authentication, and UI/API commands remain application-owned.
-
The public export allowlist omitted the owned-organization files introduced in preview 22. Including them and building the exported tree tests the actual distribution boundary rather than only comparing its path list with itself.
-
Shared business purchasing exposed the difference between an initial member and an initial RBAC owner. The historical organization creation method commits membership but no access binding. The new
CreateOwnedOrganizationextension grants the application-configured role and writes both audits atomically for an existing active, fully registered user. It rejects missing roles and unsupported adapters instead of leaving an ownerless organization behind. SQLite tests inject failure at every write stage, including the second audit, and race duplicate creates. Customer/merchant vocabulary remains application policy; there is no new database schema or commerce dependency in Foundations. -
The account email remains required and unique. Gamertan uses normalized email as the canonical login identifier and keeps username as a stable public identity. Until a mail package exists, the application must not describe an address as verified merely because it was entered during registration.
-
Password authentication is sufficient for an ordinary customer base session. Privileged application actions use an exact operation binding with
authwebauthn.BeginApprovalandFinishApproval; that is safer than a broad long-lived "elevated" session. A user without a passkey can use ordinary features but must enroll one before performing protected work. -
auth.Service.VerifyPasswordremains available for flows that truly require password plus passkey before session issuance. -
Public registration exposed a cross-package transaction boundary. The
accountpackage now keeps an unusable bounded registration draft and makes recovery-code digests, personal organization, membership, owner binding, activation, audits, and an optional initial passkey one repository commit. A failed WebAuthn ceremony can be restarted, or an ordinary password account can finish without it, without persisting a partly privileged account. -
Media belongs behind a storage-neutral interface with a hardened local adapter. Content workflow, references, and authorization remain application policy.
-
Historical
authsqlite.Openstill migrates for compatibility. Applications with reviewed deployment gates useOpenWithOptionswith migration disabled, require the current schema at startup, and invokeMigrateonly from an explicit operator command. -
Commerce remains a separately versioned nested module so payment-provider policy and catalog evolution do not enlarge the authentication core.
-
Self-service enrollment exposed an authorization seam: completing a valid ceremony and checking its user only after persistence is too late.
FinishRegistrationForUsernow consumes mismatched ceremonies and checks the application-authenticated user before storing a credential. -
First-owner provisioning exposed another cross-package transaction boundary.
bootstrapnow commits the passkey-only user, enrollment digest, non-personal organization, membership, direct owner binding, and audits together. Applications must seed their owner role first and must write the returned raw token only to a newly created private file. -
Recovery-code consumption alone is not a complete recovery path. The restricted grant must survive an interrupted authenticator prompt yet be consumed in the same transaction that stores the verified replacement passkey and replacement code digests.
authrecovery.BeginPasskeyandFinishPasskeynow provide that boundary without creating an authenticated session; Gamertan keeps the raw grant only in a short-lived HttpOnly cookie. -
A portless-only WebAuthn origin rule made an unprivileged local HTTPS exercise impossible even though WebAuthn origins include ports. The passkey service now permits an explicit development port only when applications opt in and the RP ID is
localhostor reserved.test; production origins keep the original portless default. -
Gamertan's staff-access page exposed a dangerous composition gap between individual grant/revoke calls. Foundations now owns one optimistic, transactional direct-role replacement that preserves the final active owner and appends its audit before commit. The application still owns route authorization, role presentation, CSRF, and the exact fresh-passkey operation binding.
-
Extending that page to membership suspension, reactivation, and removal exposed the same time-of-check gap in the older lifecycle methods. The new optimistic extension serializes on the active administrator membership, rechecks the exact state bound into the passkey assertion, applies team and direct-binding consequences, and writes the audit in one transaction.
-
Human-assisted recovery cannot safely be expressed as a root command behind an HTTP button. Preview 18 adds a distinct owner-assisted protocol: the application performs the human review and fresh operation-bound passkey ceremony, while the SQLite transaction rechecks an active direct owner, invalidates every old account authenticator, stores only the grant digest, and writes identity plus organization audits. Grant completion installs the replacement password, passkey, and recovery-code set atomically and never issues a session.
-
Gamertan's distinction between Site Admin and Owner exposed a second composition boundary: permission to manage ordinary staff must not imply permission to create, demote, suspend, or remove an Owner. Preview 19 moves that invariant into the same SQLite transactions as direct-role and membership changes, while leaving the application's role vocabulary and UI policy application-owned.
-
Gamertan's invitation work found the same authority boundary before a route was exposed: Site Admin must be able to invite ordinary staff without being able to grant or cancel Owner access. Preview 20 passes the configured owner role into invitation mutations and rechecks a current active direct Owner after acquiring the SQLite write lock. The application still owns fresh authentication, recipient delivery, and the one-time secret presentation.
-
A real Bitwarden/Vaultwarden owner enrollment reached successful WebAuthn verification but was rejected by a redundant algorithm check because the application's direct response serializer omitted the optional browser
publicKeyAlgorithmconvenience member. Preview 21 keeps ES256-only policy enforcement but derives it from the verified COSE key embedded in authenticator data. This makes the server independent of serializer-specific convenience fields without weakening origin, challenge, user-verification, or algorithm validation.