Authentication
WiseWig delegates identity proof to OAuth/OIDC authorization code flow with S256 PKCE. beginOAuth() generates 32 cryptographically random bytes for both state and verifier, persists only the state hash with a ten-minute expiry, and permits only same-origin relative return paths. completeOAuth() consumes state before redemption, sends the one-time verifier to the configured HTTPS token endpoint, bounds provider responses, and obtains the subject from the configured HTTPS UserInfo endpoint. This follows RFC 7636 and the authorization-code flow in RFC 6749.
In managed hosting, /api/auth/start, callback, and logout are fixed Astro proxies to the trusted dispatcher. The dispatcher resolves the verified hostname first, owns D1 OAuth state and sessions, holds the signing/client secrets, maps the external identity to current tenant/site roles, and returns the site-scoped cookie. The customer-facing Astro Worker cannot query identity tables, mint/revoke sessions, or read either secret. Its edit/preview SSR path accepts the actual mode returned by the dispatcher; an unauthorized edit hint is served as published data without local authentication work.
The external (issuer, subject) must already map to an invited WiseWig user. Authorized owners map that identity and its site role through the members interface before first login. The callback resolves the verified request hostname and authoritative roles, rotates prior sessions for that user/tenant, and creates an eight-hour D1 session record before issuing a signed HttpOnly cookie containing a unique session ID. Production page and command providers require both a valid signature and an active matching D1 record; logout revokes the record and clears the cookie. Account recovery and identity proof stay with the configured provider instead of creating a second password/recovery system in WiseWig.
@wise-wig/auth owns browser-safe Web Crypto signing and verification. It does not decide roles or authenticate passwords. The host integration supplies the signing secret from its secrets manager and resolves memberships before issuing claims.
SignedCookieSessionProvider converts a verified short-lived cookie into the tenant-scoped runtime identity. sessionCookie() emits an HttpOnly, path-scoped cookie and requires an explicit secure flag and bounded lifetime. Cookies default to SameSite=Strict; the OAuth callback deliberately emits the new session as SameSite=Lax so it is available on the immediate top-level GET at the end of the external provider’s redirect chain. The editor-intent cookie uses the same narrow exception when /api/editor/enter is that callback’s second hop. Lax does not authorize effects: cookie-authenticated mutations call assertSameOriginMutation() before parsing input or invoking a command, require the exact configured Origin, and reject cross-site Fetch Metadata.
Editor entry is intentionally separate from authentication. The extension or browser fallback completes OAuth and then visits /api/editor/enter, which sets a bounded HttpOnly wise_wig_mode=edit intent marker and returns to the ordinary page URL. That marker bypasses the public HTML cache and asks the renderer for edit mode, but carries no identity, role, tenant, or site authority. A forged marker without a valid revocable session and current content.edit permission receives published HTML with no editing metadata. Exit editor clears only the mode marker; logout revokes the D1 session and clears both cookies.
Cloudflare hosts call authorizePrivateSiteRequest() for every preview, edit, and private API request. It derives tenant and site scope from the verified request hostname, then checks the signed session and authoritative membership. Request bodies never choose their own tenant scope.
The browser extension is the primary editor entry experience. After an explicit toolbar click, its temporary activeTab plus scripting permissions read only the page’s non-secret meta[name="wise-wig-editor"] declaration. If deployment left an older public HTML entry at the edge, it falls back to the public, minimal, no-store /api/editor/discovery contract from that same page origin. It requests no persistent host permission and receives neither the provider password nor the HttpOnly WiseWig cookies. It navigates the same tab through the site’s /api/auth/start route, so the server retains the same OAuth PKCE, session, membership, and role boundary as browser-only entry. The fixed local handoff code is used only for localhost; that endpoint returns 404 on production hosts.
Browser-only entry remains supported at /api/auth/start for mobile, recovery, accessibility, and browsers without the extension. This route is not secret and does not need to be: knowing it grants no authority, just as knowing an OAuth login URL does not grant an account.