File
Blob: src/client/pages/privacy.tsx
| 1 | import type { ReactNode } from "react"; |
| 2 | |
| 3 | import { ContactEmail } from "@/client/components/ui/contact-email"; |
| 4 | import { useClientConfig } from "@/client/lib/config"; |
| 5 | |
| 6 | const Section = ({ title, children }: { title: string; children: ReactNode }) => ( |
| 7 | <section className="mt-12"> |
| 8 | <h2 className="mb-4 font-display text-xl font-semibold text-zinc-100">{title}</h2> |
| 9 | <div className="space-y-4 text-base leading-[1.7] text-zinc-300">{children}</div> |
| 10 | </section> |
| 11 | ); |
| 12 | |
| 13 | export const PrivacyPage = () => { |
| 14 | const config = useClientConfig(); |
| 15 | if (!config || config.status !== "ok") return null; |
| 16 | const { operatorName, operatorContactEmail } = config; |
| 17 | |
| 18 | return ( |
| 19 | <div className="mx-auto max-w-2xl py-12 sm:py-20"> |
| 20 | <p className="mb-6 text-[11px] font-medium uppercase tracking-[0.18em] text-zinc-500">Privacy policy</p> |
| 21 | |
| 22 | <h1 className="mb-6 font-display text-[clamp(2rem,5.5vw,3.25rem)] font-semibold leading-[1.05] tracking-tight text-zinc-100"> |
| 23 | Privacy policy |
| 24 | </h1> |
| 25 | |
| 26 | <p className="mb-10 text-sm text-zinc-500">Effective April 27, 2026.</p> |
| 27 | |
| 28 | <p className="max-w-[60ch] text-base leading-[1.7] text-zinc-300"> |
| 29 | tessera is a single sign-on provider operated by {operatorName}. This policy explains what personal information |
| 30 | tessera handles, why, and the choices you have. It applies to the service hosted at this domain and to any |
| 31 | tessera deployment operated by {operatorName}. |
| 32 | </p> |
| 33 | |
| 34 | <Section title="Invite-only by design"> |
| 35 | <p> |
| 36 | tessera does not offer open registration. You can use the service only after an administrator has minted an |
| 37 | invite for your email address and you have accepted it. Signing in with Google or GitHub does not create a new |
| 38 | account; it works only after you have already signed in with email and password and explicitly linked that |
| 39 | identity from your account page. If a Google or GitHub sign-in does not match an existing tessera account, the |
| 40 | sign-in is rejected and no profile data is retained. |
| 41 | </p> |
| 42 | </Section> |
| 43 | |
| 44 | <Section title="What we collect"> |
| 45 | <p> |
| 46 | <strong className="text-zinc-100">Account information.</strong> Your email address, display name, and a |
| 47 | slug-safe handle derived from your name at account creation. If you set a password, we store an Argon2id hash |
| 48 | of it; we never store the password itself. |
| 49 | </p> |
| 50 | <p> |
| 51 | <strong className="text-zinc-100">Linked identities.</strong> If you link a Google or GitHub identity from |
| 52 | your account page, we receive and store the provider's account identifier, your email, and your name as |
| 53 | returned by the provider. We also receive access and refresh tokens from the provider; these are encrypted |
| 54 | with a server secret before being written to our database. |
| 55 | </p> |
| 56 | <p> |
| 57 | <strong className="text-zinc-100">Sessions.</strong> When you sign in we set a session cookie scoped to this |
| 58 | domain. The corresponding session row in our database records the session identifier, an expiry, and the IP |
| 59 | address and user-agent that started the session. |
| 60 | </p> |
| 61 | <p> |
| 62 | <strong className="text-zinc-100">Operational data.</strong> Our hosting provider, Cloudflare, sees the IP |
| 63 | address of every request and may retain short-lived edge logs for abuse prevention and rate limiting. Sign-in |
| 64 | and invite acceptance flows are protected by Cloudflare Turnstile, which evaluates a one-time challenge token; |
| 65 | we do not retain the raw challenge response. |
| 66 | </p> |
| 67 | <p>tessera does not run third-party analytics, advertising, or tracking scripts.</p> |
| 68 | </Section> |
| 69 | |
| 70 | <Section title="How we use it"> |
| 71 | <p> |
| 72 | We use the information above only to operate tessera: to authenticate you, to issue OpenID Connect identity |
| 73 | tokens to applications you choose to sign in to, to enforce rate limits and abuse protections, and to let |
| 74 | administrators manage invites, clients, and user roles. |
| 75 | </p> |
| 76 | <p> |
| 77 | Information that Google returns to tessera through the Google sign-in flow is used solely to authenticate you |
| 78 | to your existing tessera account. We do not use Google profile data to build advertising profiles, to train |
| 79 | machine-learning models, or for any purpose unrelated to the authentication you initiated. Tessera's use of |
| 80 | information received from Google APIs adheres to the{" "} |
| 81 | <a |
| 82 | href="https://developers.google.com/terms/api-services-user-data-policy" |
| 83 | target="_blank" |
| 84 | rel="noopener noreferrer" |
| 85 | className="text-zinc-100 underline decoration-zinc-700 underline-offset-[6px] transition-colors hover:text-accent-400 hover:decoration-accent-500/60" |
| 86 | > |
| 87 | Google API Services User Data Policy |
| 88 | </a> |
| 89 | , including the Limited Use requirements. |
| 90 | </p> |
| 91 | </Section> |
| 92 | |
| 93 | <Section title="Sub-processors"> |
| 94 | <p>tessera relies on a small number of providers to operate the service:</p> |
| 95 | <ul className="list-disc space-y-2 pl-6"> |
| 96 | <li> |
| 97 | <strong className="text-zinc-100">Cloudflare</strong> hosts the application (Workers, D1, Turnstile, |
| 98 | rate-limiting). Data is stored in Cloudflare's infrastructure and may transit through edge locations |
| 99 | worldwide. |
| 100 | </li> |
| 101 | <li> |
| 102 | <strong className="text-zinc-100">Google</strong> is contacted only when you initiate a Google sign-in and |
| 103 | receives the OAuth request parameters required to authenticate you. |
| 104 | </li> |
| 105 | <li> |
| 106 | <strong className="text-zinc-100">GitHub</strong> is contacted only when you initiate a GitHub sign-in and |
| 107 | receives the OAuth request parameters required to authenticate you. |
| 108 | </li> |
| 109 | </ul> |
| 110 | <p>We do not sell, rent, or share your personal information with anyone else.</p> |
| 111 | </Section> |
| 112 | |
| 113 | <Section title="Where data is stored, and for how long"> |
| 114 | <p> |
| 115 | Account, session, invite, and OAuth-client data is stored in Cloudflare D1. We retain account information for |
| 116 | as long as your account exists. Sessions are deleted when they expire or when you sign out. Audit records used |
| 117 | for abuse prevention may be retained briefly by Cloudflare's edge logs. |
| 118 | </p> |
| 119 | <p> |
| 120 | When you delete your account, your user row, linked identities, sessions, and stored OAuth tokens are removed. |
| 121 | Backups, if any, are rotated on a short cadence and overwritten in due course. |
| 122 | </p> |
| 123 | </Section> |
| 124 | |
| 125 | <Section title="Your choices"> |
| 126 | <p>From your account page you can:</p> |
| 127 | <ul className="list-disc space-y-2 pl-6"> |
| 128 | <li>Update your display name and handle.</li> |
| 129 | <li>Change your password.</li> |
| 130 | <li>Link or unlink a Google or GitHub identity.</li> |
| 131 | <li>Sign out of any active session, including remote ones.</li> |
| 132 | <li>See which OAuth applications have been authorized to receive your identity, and revoke them.</li> |
| 133 | </ul> |
| 134 | <p> |
| 135 | To delete your tessera account or to request a copy of the personal information we hold about you, email{" "} |
| 136 | <ContactEmail address={operatorContactEmail} /> from the address on file. If you are a California resident, |
| 137 | you have the right to know, delete, correct, and limit the use of your personal information under the |
| 138 | California Consumer Privacy Act; we honor these rights regardless of where you reside. |
| 139 | </p> |
| 140 | <p> |
| 141 | You can also revoke tessera's access to your Google account at any time from your{" "} |
| 142 | <a |
| 143 | href="https://myaccount.google.com/permissions" |
| 144 | target="_blank" |
| 145 | rel="noopener noreferrer" |
| 146 | className="text-zinc-100 underline decoration-zinc-700 underline-offset-[6px] transition-colors hover:text-accent-400 hover:decoration-accent-500/60" |
| 147 | > |
| 148 | Google account permissions page |
| 149 | </a> |
| 150 | . |
| 151 | </p> |
| 152 | </Section> |
| 153 | |
| 154 | <Section title="Cookies"> |
| 155 | <p> |
| 156 | tessera sets a session cookie when you sign in and may set short-lived cookies during the OAuth flow to |
| 157 | protect against cross-site request forgery. We do not use cookies for advertising or analytics. |
| 158 | </p> |
| 159 | </Section> |
| 160 | |
| 161 | <Section title="Security"> |
| 162 | <p> |
| 163 | Passwords are stored as Argon2id hashes. OAuth access and refresh tokens are encrypted with a server secret |
| 164 | before storage. Identity tokens are signed with keys whose private halves are encrypted at rest and rotated on |
| 165 | a regular cadence. Connections to tessera are served over TLS. |
| 166 | </p> |
| 167 | <p> |
| 168 | No system is perfectly secure; if you discover a vulnerability, please email{" "} |
| 169 | <ContactEmail address={operatorContactEmail} />. |
| 170 | </p> |
| 171 | </Section> |
| 172 | |
| 173 | <Section title="International transfers"> |
| 174 | <p> |
| 175 | tessera is operated from the United States and uses providers that may process data in the United States and |
| 176 | other countries. By using tessera you consent to the transfer of your information to these locations. |
| 177 | </p> |
| 178 | </Section> |
| 179 | |
| 180 | <Section title="Children"> |
| 181 | <p> |
| 182 | tessera is not directed to children under 13. We do not knowingly collect personal information from children |
| 183 | under 13. If you believe a child has provided us with personal information, contact us and we will remove it. |
| 184 | </p> |
| 185 | </Section> |
| 186 | |
| 187 | <Section title="Changes to this policy"> |
| 188 | <p> |
| 189 | We may update this policy from time to time. The effective date at the top of this page indicates when it was |
| 190 | last revised. Material changes will be communicated through the service or by email to the address on file. |
| 191 | </p> |
| 192 | </Section> |
| 193 | |
| 194 | <Section title="Contact"> |
| 195 | <p> |
| 196 | Questions about this policy or about your information can be sent to{" "} |
| 197 | <ContactEmail address={operatorContactEmail} />. |
| 198 | </p> |
| 199 | </Section> |
| 200 | </div> |
| 201 | ); |
| 202 | }; |