From 46466ba1dc34ff67ca03b4911821335dca8ff2ea Mon Sep 17 00:00:00 2001 From: Abdullah Sultan Date: Thu, 6 Aug 2026 12:36:07 -0500 Subject: [PATCH 1/2] Add the privacy suite: policy, cookies, subprocessors, DPA, breach runbook Four new legal pages, each written from the code rather than from a template, plus a breach-response runbook in the security model. - docs/privacy-policy.md: the full Art. 13/14 notice. Data inventory traced to the tables and modules that hold each category, including the one people forget -- third parties whose personal data sits inside synced files. Retention stated per category, with an honest note that the 7-day sweep is a serving guarantee and not a verified automated delete: there is no cron entry, scheduled function or pg_cron job anywhere that performs it. - docs/subprocessors.md: six vendors, verified by grepping hostnames rather than assumed. Supabase runs in ca-central-1 (Montreal), not the US -- probed per-region against the connection pooler. 30 days' notice, how to object. - docs/cookies.md: every storage item by name. Written for the post-removal state of crosscode_distinct_id, which workstream E is landing. - docs/dpa.md: Art. 28 processing agreement, accept-by-reference. The Art. 32 section says plainly that there is no end-to-end encryption, and lists what is *not* in place as well as what is. - docs/security.md: breach-response runbook. 72-hour clock, notification templates for the authority and for users, evidence preservation before provider logs age out. docs/privacy.md gets cross-links only; it stays the plain-language summary. Plumbing: all four registered in generate-docs.mjs, vite.config.js and both footers. Kept workstream C's stripHtmlComments() helper -- without it the notes render as visible text. See HANDOFF.md. Every {{TOKEN}} used is one of the four approved ones and each page ends with a fill-in checklist naming them. Co-Authored-By: Claude Opus 5 (1M context) --- HANDOFF.md | 61 +++++ apps/docs-site/.gitignore | 4 + apps/docs-site/index.html | 4 + apps/docs-site/scripts/generate-docs.mjs | 57 +++- apps/docs-site/vite.config.js | 4 + docs/cookies.md | 85 ++++++ docs/dpa.md | 322 +++++++++++++++++++++++ docs/privacy-policy.md | 291 ++++++++++++++++++++ docs/privacy.md | 10 + docs/security.md | 207 +++++++++++++++ docs/subprocessors.md | 76 ++++++ 11 files changed, 1118 insertions(+), 3 deletions(-) create mode 100644 HANDOFF.md create mode 100644 docs/cookies.md create mode 100644 docs/dpa.md create mode 100644 docs/privacy-policy.md create mode 100644 docs/subprocessors.md diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..3d5f70d --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,61 @@ +# Handoff from workstream B (privacy suite) + +Changes I wanted to make in files another workstream owns. Not made. + +## `docs/terms.md` — workstream A + +1. **Incorporate the DPA by reference.** `docs/dpa.md` is written to apply automatically + ("accept-by-reference from the terms"), but nothing in the terms currently says so, which + means it does not actually apply. Add to §5 (*Your content, and who owns it*) or §1: + + > Where the files you sync contain personal data belonging to other people, you are the + > controller of that data and we are your processor. The [Data Processing + > Agreement](/docs/dpa.html) sets out the terms on which we process it and forms part of + > these terms. You do not need to sign or request it. + +2. **Point at the new privacy pages.** §6 and the closing links reference + `docs/privacy.md` only. Add `/docs/privacy-policy.html`, `/docs/cookies.html` and + `/docs/subprocessors.html`. + +3. **Age.** `docs/privacy-policy.md` §11 states minimum age 16 and cites terms §3. Keep the + two in step if §3 changes. + +## `docs/privacy.md` — a claim I could not verify + +Under *How long it is kept*: "Older changes are deleted by a scheduled job." + +**There is no such job in this repository.** I grepped the source tree for a cron entry, a +Vercel cron config, a scheduled function and a `DELETE FROM file_versions`, and queried +`cron.job` on the production Supabase database — the `pg_cron` schema does not exist there. +What the code does enforce is the *serving* side: `listChanges` refuses a cursor pointing at +history older than what survives, and reports `retentionDays: 7` +(`apps/service/src/store.ts:458`). + +I did not edit `privacy.md` beyond the cross-links my brief allows. I documented the gap +honestly in `docs/privacy-policy.md` §8 and flagged it with a `` comment. +Someone should either land the sweep as a scheduled job with a test, or soften the sentence +in `privacy.md`. + +## `apps/docs-site/scripts/generate-docs.mjs` — `stripHtmlComments`, shared with workstream C + +Workstream C's edits arrived in this worktree mid-run (Crosscode was syncing the two +checkouts). I removed C's page registrations from my branch so it builds standalone, but I +**kept** C's `stripHtmlComments()` helper — without it the `` notes in +my four pages render as escaped visible text on the published site, because the renderer +runs with `html: false`. If C's branch merges first, this is the same function twice and +the merge is a no-op. If mine merges first, C's is redundant. Either way, keep exactly one. + +## `docs/support.md` — whoever owns it + +It still contains the literal placeholder `[SUPPORT EMAIL]` in three table rows. The real +address is `support@getcrosscode.dev`. + +## `apps/docs-site/src/analytics.js` — workstream E + +`docs/cookies.md` is written for the post-removal state of `crosscode_distinct_id`, as my +brief instructs. If that removal does not land, the cookies page is wrong. Its "Before this +takes effect" checklist says so. + +Same file: `docs/observability.md` notes the module is not included on +`apps/docs-site/auth/signup.html`, so `sign_up_completed` never fires. Not mine to fix, but +it affects what the analytics section of the privacy policy describes. diff --git a/apps/docs-site/.gitignore b/apps/docs-site/.gitignore index 8bba99b..3c91044 100644 --- a/apps/docs-site/.gitignore +++ b/apps/docs-site/.gitignore @@ -11,6 +11,10 @@ dist /docs/support.html /docs/terms.html /docs/refund-policy.html +/docs/privacy-policy.html +/docs/cookies.html +/docs/subprocessors.html +/docs/dpa.html /public/docs/ /public/llms.txt /public/llms-full.txt diff --git a/apps/docs-site/index.html b/apps/docs-site/index.html index e906507..f40cfae 100644 --- a/apps/docs-site/index.html +++ b/apps/docs-site/index.html @@ -500,6 +500,10 @@

Docs

Trust

Safety model Privacy + Privacy Policy + Cookies + Subprocessors + Data Processing Agreement Limitations Terms Support diff --git a/apps/docs-site/scripts/generate-docs.mjs b/apps/docs-site/scripts/generate-docs.mjs index b65cc09..2ab40ae 100644 --- a/apps/docs-site/scripts/generate-docs.mjs +++ b/apps/docs-site/scripts/generate-docs.mjs @@ -97,7 +97,11 @@ const NAV_GROUPS = [ label: "Support & legal", links: [ { href: "/docs/support.html", label: "Support", key: "support" }, - { href: "/docs/terms.html", label: "Terms of Service", key: "terms" } + { href: "/docs/terms.html", label: "Terms of Service", key: "terms" }, + { href: "/docs/privacy-policy.html", label: "Privacy Policy", key: "privacy-policy" }, + { href: "/docs/cookies.html", label: "Cookies", key: "cookies" }, + { href: "/docs/subprocessors.html", label: "Subprocessors", key: "subprocessors" }, + { href: "/docs/dpa.html", label: "Data Processing Agreement", key: "dpa" } ] } ]; @@ -159,6 +163,38 @@ const GENERATED_PAGES = [ title: "Terms of Service", mdFile: "terms.md", htmlOut: "terms.html", + nextHref: "/docs/privacy-policy.html", + nextLabel: "Privacy Policy" + }, + { + key: "privacy-policy", + title: "Privacy Policy", + mdFile: "privacy-policy.md", + htmlOut: "privacy-policy.html", + nextHref: "/docs/cookies.html", + nextLabel: "Cookies and local storage" + }, + { + key: "cookies", + title: "Cookies and local storage", + mdFile: "cookies.md", + htmlOut: "cookies.html", + nextHref: "/docs/subprocessors.html", + nextLabel: "Subprocessors" + }, + { + key: "subprocessors", + title: "Subprocessors", + mdFile: "subprocessors.md", + htmlOut: "subprocessors.html", + nextHref: "/docs/dpa.html", + nextLabel: "Data Processing Agreement" + }, + { + key: "dpa", + title: "Data Processing Agreement", + mdFile: "dpa.md", + htmlOut: "dpa.html", nextHref: "/docs/index.html", nextLabel: "Docs overview" } @@ -241,19 +277,34 @@ ${bodyHtml} `; } +// Legal docs carry `` notes flagging choices that need review. The +// renderer runs with html:false, so a comment left in would be escaped and printed on the +// page as visible text. Strip them from everything published; the repo's docs/*.md keeps +// them, which is where they are meant to be read. +function stripHtmlComments(markdown) { + return markdown.replace(/^[ \t]*[ \t]*\r?\n?/gm, ""); +} + function generateDocPages() { mkdirSync(siteDocsDir, { recursive: true }); mkdirSync(publicDocsDir, { recursive: true }); for (const p of GENERATED_PAGES) { const mdPath = path.join(rootDocsDir, p.mdFile); - const mdContent = readFileSync(mdPath, "utf8"); + const mdContent = stripHtmlComments(readFileSync(mdPath, "utf8")); const bodyHtml = md.render(mdContent); const mdOutName = p.mdOutName ?? p.mdFile; const mdHref = `/docs/${mdOutName}`; @@ -360,7 +411,7 @@ function generateLlmsFullTxt() { ]; for (const s of sections) { - const content = readFileSync(path.join(rootDocsDir, s.file), "utf8"); + const content = stripHtmlComments(readFileSync(path.join(rootDocsDir, s.file), "utf8")); parts.push("---", "", ``, "", content.trimEnd(), ""); } diff --git a/apps/docs-site/vite.config.js b/apps/docs-site/vite.config.js index 04801a3..bd5f82f 100644 --- a/apps/docs-site/vite.config.js +++ b/apps/docs-site/vite.config.js @@ -23,6 +23,10 @@ export default defineConfig({ limitations: resolve(root, "docs/limitations.html"), support: resolve(root, "docs/support.html"), terms: resolve(root, "docs/terms.html"), + privacyPolicy: resolve(root, "docs/privacy-policy.html"), + cookies: resolve(root, "docs/cookies.html"), + subprocessors: resolve(root, "docs/subprocessors.html"), + dpa: resolve(root, "docs/dpa.html"), signin: resolve(root, "auth/signin.html"), signup: resolve(root, "auth/signup.html"), passwordReset: resolve(root, "auth/reset.html"), diff --git a/docs/cookies.md b/docs/cookies.md new file mode 100644 index 0000000..0a14c99 --- /dev/null +++ b/docs/cookies.md @@ -0,0 +1,85 @@ +# Cookies and local storage + +> **Draft.** Not yet in force. See the checklist at the end. + +There is no cookie banner on this site, and this page still has to exist. The ePrivacy +Directive governs *any* storing of or access to information on your device — `localStorage` +and `sessionStorage` are covered exactly as cookies are — and disclosure is required even +where the item is strictly necessary and consent is not. + +So: every item, what it is for, how long it lasts, and whether it needs your consent. + +Effective from {{EFFECTIVE_DATE}}. + +## What we set + +| Name | Type | Set by | Purpose | Duration | Strictly necessary? | +| --- | --- | --- | --- | --- | --- | +| `sb-rzsslbmahvoesjxmgefr-auth-token` | `localStorage` | Supabase Auth (`@supabase/supabase-js`), on the sign-in, sign-up, reset, join and device pages | Holds your session and refresh token so you stay signed in across pages and can complete a CLI device sign-in | Until you sign out or the refresh token is revoked | **Yes.** Consent-exempt: without it there is no sign-in | +| `crosscode_analytics_once:` | `sessionStorage` | `apps/docs-site/src/analytics.js` | Marks that a funnel event (`sign_up_started`, `sign_up_completed`) has already been counted in this tab, so two triggers do not count it twice | Until the browser tab is closed | **No.** See "Why there is no banner" below | +| `__cf_bm` | Cookie, on `supabase.co` | Cloudflare, in front of Supabase | Bot management on the auth API the sign-in pages call | 30 minutes | **Yes.** Set by Supabase's infrastructure, not by Crosscode, and not readable by this site | + +That is the whole list. There is no analytics cookie, no advertising cookie, no +session-recording script, no chat widget, no CDN font, and no third-party tag: the only +external hostname in this site's HTML is `github.com`. + +## What we used to set, and no longer do + +`crosscode_distinct_id` — a random UUID in `localStorage` that gave each browser a stable +identity across visits, so PostHog could tell a returning visitor from a new one. + +It has been removed. Website analytics events now go out with no persistent identifier, +which means they cannot be linked into a per-visitor history. It is a real loss of signal +and worth it on a site whose pitch is that we hold your code and would rather say so +plainly than track you around a page about it. + +If you visited before the change, the old value may still be sitting in your browser's +local storage. Nothing reads it any more. Clearing site data removes it. + +## Why there is no banner + +Strictly necessary items are consent-exempt, so the auth token and `__cf_bm` need no +banner. + +That leaves `crosscode_analytics_once:*`. It is a per-tab flag holding the string `"1"`; it +identifies nobody, carries no identifier, is deleted when the tab closes, and exists solely +to stop one event being counted twice. + + + +## Turning it off + +- **Analytics:** any content blocker that blocks `us.i.posthog.com` stops the events. So + does denying storage for this site — the code falls back and sends the event without the + dedup flag rather than breaking. A build with no PostHog key installs no listeners and + sends nothing at all, which is what a fork or a local `pnpm docs:dev` runs. +- **The auth token:** clearing site data signs you out. There is no way to stay signed in + without it. +- **Do Not Track / Global Privacy Control:** not currently honoured as a signal. Stated + because claiming otherwise without the code to back it would be worse. + +## The CLI and daemon + +Not a browser, no cookies. The CLI stores your session in +`/crosscode/config.json` at mode `0600`, preferring the OS keychain for the +refresh token where one exists (macOS `security`, Linux `secret-tool`). See the +[security model](/docs/safety.html). + +## Related + +- [Privacy Policy](./privacy-policy.md) — the complete Art. 13/14 notice +- [Privacy: what we can and can't see](./privacy.md) — the plain-language summary +- [Subprocessors](./subprocessors.md) — who else touches this data + +## Before this takes effect + +- `{{EFFECTIVE_DATE}}` — the date this page takes effect. +- Confirm the `crosscode_distinct_id` removal has actually shipped to production before + publishing. This page describes the post-removal state. +- Resolve the `` note above on `crosscode_analytics_once:*`. +- If the Supabase project ref ever changes, the auth token key name in the table changes + with it — it is `sb--auth-token`. diff --git a/docs/dpa.md b/docs/dpa.md new file mode 100644 index 0000000..4eb86e2 --- /dev/null +++ b/docs/dpa.md @@ -0,0 +1,322 @@ +# Data Processing Agreement + +> **Draft.** Not yet in force. The double-brace placeholders are unfilled and the SCC annex in +> §13 is a placeholder. Do not rely on this as a signed agreement until the checklist at the +> end is done. + +This is the Article 28 agreement between you and Crosscode for the personal data that ends +up inside the files you sync. It applies automatically when you use the hosted service — +you do not have to sign, request or negotiate anything. If you need a counter-signed copy +for your own records, write to **legal@getcrosscode.dev**. + +It is short on purpose. Everything in it is a commitment a one-person project can actually +keep. + +Effective from {{EFFECTIVE_DATE}}. + +## 1. Parties and roles + +- **Controller:** you — the account holder, or the organisation you sync on behalf of. +- **Processor:** {{PROVIDER_NAME}}, an individual acting as a sole proprietor, at + {{PROVIDER_ADDRESS}} ("Crosscode"). + +You decide why and how the personal data inside your repositories is processed. Crosscode +processes it only to run the sync service, on your instructions. + +For your own account data — your email, your GitHub identity, your projects — Crosscode is +the **controller**, not your processor, and the [Privacy Policy](./privacy-policy.md) +governs instead. This agreement covers only the data inside your files. + +## 2. Subject matter, nature and purpose + +**Subject matter.** Real-time synchronisation of uncommitted working-tree files between Git +checkouts of the same repository. + +**Nature of the processing.** Receiving file changes from your checkouts, storing them, +relaying them to your other checkouts and to members you have invited, and deleting them +when the retention window passes. Also: authenticating your users, deciding who may sync +with whom, recording errors, and counting usage. + +**Purpose.** To perform the sync service for you. Nothing else. In particular the data is +never used to train a model, is never sold or shared with advertisers, and is never +processed for Crosscode's own purposes beyond the security, error-monitoring and +usage-counting described in §5 of the [Privacy Policy](./privacy-policy.md). + +**Duration.** From when you first sync until you stop and ask for deletion, or the account +ends. + +## 3. Types of personal data + +Whatever your repositories contain. Crosscode does not inspect file contents and cannot +categorise them, so this is stated by shape rather than by list: + +- Personal data written into source code and configuration: names and email addresses in + comments, `CODEOWNERS`, authorship metadata. +- Personal data in test fixtures, seed data, sample payloads, screenshots and database + dumps: names, email addresses, postal addresses, order and customer records, and anything + else that ends up in a fixture. +- Anything else a tracked file happens to hold. + +**Two limits that reduce this materially.** Only files Git already *tracks* are ever sent — +untracked files never leave the machine. And a path denylist drops `.env*`, `.envrc`, +`.npmrc`, `.netrc`, `credentials*`, `secret*`, SSH private keys, and `*.pem` / `*.key` / +`*.p12` / `*.pfx` / `*.jks` / `*.keystore` before a change is captured, rather than +filtering them later. `packages/core/src/index.ts` is the authoritative list. + +**Special category data (Art. 9) and criminal offence data (Art. 10) are outside the scope +of this agreement.** The service is not designed for them and you must not sync +repositories that contain them. + +## 4. Categories of data subject + +Whoever your repositories describe: your customers and end users, your employees and +contributors, and third parties named in code, fixtures or documentation. + +## 5. Your instructions + +Crosscode processes the data only on your documented instructions. Using the service is +the instruction: syncing a checkout instructs us to store and relay its tracked file +changes. + +Further instructions go to **privacy@getcrosscode.dev**. We will follow reasonable ones +that the service is technically capable of. If an instruction would require a feature that +does not exist, we will say so rather than agree and not do it. + +If an instruction would breach data protection law, we will tell you and may pause that +processing. + +Crosscode will not transfer the data to a third country except as described in §7 and §13, +and will tell you if legally required to do so unless the law forbids the telling. + +## 6. Confidentiality + +Exactly one person has production access: {{PROVIDER_NAME}}. There is no team, so there is +no list of authorised personnel to maintain and nobody else to bind. That person is bound +to confidentiality by this agreement. + +If that ever stops being true — a second maintainer, a contractor with production access — +each such person will be under a written confidentiality obligation before they get access, +and this section will be updated to say so. + +## 7. Security measures (Art. 32) + +What the code actually does. Written to be checkable against the repository, not to sound +reassuring. + +### 7.1 The one thing to understand first + +**There is no end-to-end encryption.** Files are encrypted in transit and at rest, but +under keys Crosscode manages, which means someone with production access could read your +file contents. A DPA that implied otherwise would be worse than no DPA. + +Crosscode built E2E encryption once and removed it: it cost a device-pairing ceremony, a +keyring, epochs, rotation and recovery paths, for a product whose durable artifact is a Git +repository you already have. That trade is stated openly in the +[security model](/docs/safety.html) and it is the trade you are accepting here. + +If a repository cannot be read by us, do not sync it. + +### 7.2 Encryption + +- **In transit:** TLS everywhere. Database connections require `sslmode=verify-full` — both + certificate chain and hostname are verified, with a private root supplied where the + managed provider uses one. A non-loopback connection string without `verify-full` is + refused at startup, not warned about. +- **At rest:** provider-managed encryption on Supabase's storage layer. + +### 7.3 Minimisation before capture + +- **Tracked files only.** Untracked files are never eligible. +- **The secret denylist in §3**, enforced before a change is captured rather than filtered + after. +- **Nothing outside the working tree.** Symlinks that leave the checkout are refused. +- **Retention.** Change history is kept for about 7 days + (`HISTORY_RETENTION_DAYS = 7`) and then deleted. See the honest note in §8 of the + [Privacy Policy](./privacy-policy.md) about the state of the deletion job. + +### 7.4 Access control + +- Sign-in is GitHub OAuth or email/password through Supabase Auth. The service verifies + access tokens against Supabase's JWKS and takes identity from the verified claims, never + from anything the client asserts. +- Project membership is re-derived server-side on **every** request rather than trusted from + a scope in the token, so a removed member loses access on their next request. +- Invite redemption verifies through GitHub that the redeeming account can actually read + the repository. A valid code held by someone without repo access is refused. +- CLI sign-in is a device-code handshake: no local listener, two codes of deliberately + unequal power, only a SHA-256 of the device code is stored, ~15-minute expiry, + single-use, and the poll route is rate-limited. +- Row Level Security is on from the first migration. The service connects with a + least-privilege role that cannot UPDATE, DELETE or TRUNCATE the change log; retention + runs on a separate connection. + +### 7.5 Integrity + +The receiving daemon verifies every change before applying it: content must hash to the +`contentHash` on the wire, and the merge base must resolve to the `baseHash` the sender +recorded. A change failing either is not applied. The check is deliberately the receiver's +— a service willing to substitute content would substitute the hash beside it. + +### 7.6 Telemetry hygiene + +Error reports are built field by field from an allowlist. The message goes through +`redact()`, which strips quoted spans, anything path-shaped, filenames with a known +extension, and runs of 24+ opaque characters. Routes are templated so identifiers become +`:id`. Request bodies, headers, cookies and environment are never read. Analytics +properties are allowlisted and carry no content, paths, branch names, repository names, +emails or GitHub logins. + +### 7.7 What is not in place + +Stated because omitting it is the thing that makes a security annex dishonest: + +- No SOC 2, ISO 27001 or any third-party audit. +- No penetration test. +- No formal business continuity or disaster recovery plan beyond the managed provider's + automated backups. +- No 24/7 on-call. One person, best effort. +- Availability, resilience and restore testing (Art. 32(1)(b)–(c)) rest on Supabase's and + Vercel's platform guarantees, not on anything Crosscode operates. + +## 8. Sub-processors + +You give **general written authorisation** for the sub-processors listed on +[Subprocessors](./subprocessors.md), and for changes to that list on the terms stated +there: **30 days' advance notice**, a right to object within that window, and emergency +replacement with notice within 3 business days where a vendor fails or must be dropped for +a security reason. + +Each sub-processor is engaged under a written agreement imposing data protection +obligations no less protective than these. Crosscode remains fully liable to you for their +performance. + +## 9. Assisting you + +**Data subject requests.** If a data subject contacts Crosscode about data inside your +repository, we will not respond substantively — we will tell them to contact you, and tell +you within **5 business days** if we can identify you. Realistically we usually cannot +identify whose data is in a file, because identifying it would mean reading your files. + +We will help you answer a request as far as the service technically allows. In practice +that means deleting a project, deleting an account, or deleting the change history attached +to either. Crosscode cannot search inside stored file contents for one person's data, and +would not, because doing so means reading your code. + +**DPIAs and prior consultation.** We will give you the information described in this +agreement and in the [Privacy Policy](./privacy-policy.md) to support an Art. 35 assessment +or an Art. 36 consultation. That information is this documentation; there is no +questionnaire desk behind it. + +## 10. Personal data breach + +**Crosscode will notify you without undue delay and in any event within 48 hours** of +becoming aware of a personal data breach affecting your data. 48 hours, not 72, so that you +retain time inside your own 72-hour Art. 33 clock. + +The notification will describe the nature of the breach, the categories and approximate +number of data subjects and records affected, the likely consequences, and the measures +taken or proposed — or, where the facts are not yet established, what is known so far, +followed by updates as the picture firms up. Notice goes to the email address on the +account, from **security@getcrosscode.dev**. + +Notifying **your** supervisory authority and **your** data subjects is your decision and +your obligation as controller. Crosscode will give you what you need to make it. + +The breach runbook that produces this notification is in the +[security model](/docs/safety.html#breach-response-runbook). + +## 11. Deletion and return + +On termination, or on your request at any time, Crosscode will delete your projects, their +change history, and your account records. + +**Return.** There is nothing to hand back. The data Crosscode holds is a ~7-day log of +changes to files you already have, in a repository on your own disk that is the durable +artifact the whole time. If you want an export anyway, ask within 30 days of termination +and we will provide what the service can produce. + +**The limits, stated plainly:** + +- **Backups.** Deleted rows survive in the managed provider's automated backups until those + backups age out on the provider's schedule. They are not restored except to recover from + a failure, and if a restore ever reinstates deleted data it is re-deleted. +- **Copies teammates already hold.** Every member of a project had a full checkout. + Deletion here cannot reach a copy someone already has, and no product can. + +## 12. Audit + +You may verify compliance with this agreement by: + +1. **Reading the source.** Crosscode is MIT licensed and public at + `github.com/amsultan2010/crosscode`. Every security claim in §7 names the file that + implements it. This is a stronger audit right than most processors offer and it costs + you nothing. +2. **Asking.** Written questions to **privacy@getcrosscode.dev** get a written answer + within 30 days, at most once per 12 months absent a breach or a regulator's demand. +3. **Sub-processor reports.** We will pass on whatever certifications and audit reports + Supabase and Vercel make available to their customers. + +**On-site inspection is not offered.** There is no site — one person, no office, no +datacentre of our own. An on-premises audit right would be a promise that could not be +kept. If your compliance programme requires one, Crosscode is not a suitable processor for +that workload, and it is better that you learn it here than after signing. + +## 13. International transfers + +Sub-processors are in the United States and Canada; the per-vendor detail and mechanism is +on [Subprocessors](./subprocessors.md). + +Where the EU Standard Contractual Clauses are needed for a transfer under this agreement, +Module Three (processor to sub-processor) applies, together with the UK Addendum for UK +transfers, and they are incorporated by reference with the annexes completed as follows: + +- **Annex I(A), Parties** — you as data exporter; {{PROVIDER_NAME}}, {{PROVIDER_ADDRESS}} + as data importer. +- **Annex I(B), Description of transfer** — §§2, 3 and 4 of this agreement. +- **Annex I(C), Competent supervisory authority** — the authority competent for you as + controller. +- **Annex II, Technical and organisational measures** — §7 of this agreement, in full, + including §7.7. +- **Annex III, Sub-processors** — the [Subprocessors](./subprocessors.md) page as it stands + from time to time. + + + +## 14. General + +**Governing law and venue:** {{JURISDICTION}}. Where the SCCs apply, their own governing +law and forum clauses prevail over this section for the transfers they cover. + +**Precedence.** For the personal data inside your files, this agreement prevails over the +[Terms of Service](./terms.md) if the two conflict. For everything else the Terms govern. + +**Changes.** Material changes get 30 days' notice by the same route as §8. Every version of +this page is in the repository's Git history. + +**Liability.** The limitation of liability in the [Terms of Service](./terms.md) applies to +this agreement, except where the GDPR or the SCCs make a limitation unenforceable, in which +case the statutory position governs. + +## Before this takes effect + +Fill in, in this file: + +- `{{PROVIDER_NAME}}` — appears in §1, §6, and Annex I(A) in §13 +- `{{PROVIDER_ADDRESS}}` — appears in §1 and Annex I(A) in §13 +- `{{JURISDICTION}}` — §14 +- `{{EFFECTIVE_DATE}}` — the header + +And resolve, before publishing: + +- The `` note in §13 on the SCC annexes and the missing transfer impact + assessment. +- Add an accept-by-reference clause to the [Terms of Service](./terms.md) pointing here, so + this agreement is actually incorporated. That file is owned by another workstream — see + `HANDOFF.md`. +- Decide whether the 48-hour breach deadline in §10 is one you can meet as a single person. + It is deliberately tighter than the 72 hours the law gives your controller. If it is not + realistic, change it before publishing rather than miss it later. diff --git a/docs/privacy-policy.md b/docs/privacy-policy.md new file mode 100644 index 0000000..86e2938 --- /dev/null +++ b/docs/privacy-policy.md @@ -0,0 +1,291 @@ +# Privacy Policy + +> **Draft.** This page is written but not yet in force. The double-brace placeholders below are +> unfilled. Do not rely on it as a legal document until the checklist at the end is done. + +This is the complete notice required by Articles 13 and 14 of the UK/EU GDPR. It is long +because those articles are long. + +If you want the short honest version — what the service can and cannot see — read +[Privacy: what we can and can't see](./privacy.md). That page is the summary; this page is +the complete one. Where the two disagree, this page governs, but they are not meant to +disagree. + +Effective from {{EFFECTIVE_DATE}}. + +## 1. Who is responsible + +The controller for the hosted Crosscode coordination service is {{PROVIDER_NAME}}, an +individual acting as a sole proprietor, at {{PROVIDER_ADDRESS}}. + +There is no company. Crosscode is a pre-1.0 side project run by one person, and that person +is the one who answers privacy mail. + +Contact: **privacy@getcrosscode.dev** + +There is no Data Protection Officer. The service does not meet any of the Art. 37(1) +thresholds that would require one. + + + +## 2. What this covers + +The **hosted coordination service** at `www.getcrosscode.dev`: the website, the sign-in +flow, the API the daemon talks to, and the docs site. + +It does not cover the Crosscode software itself. The CLI, daemon and MCP server are MIT +licensed and run on your machine. Running them without signing in sends nothing anywhere. + +## 3. Categories of personal data + +### 3.1 Data about you, the account holder + +| Category | What exactly | Where in the code | +| --- | --- | --- | +| Account identity | Supabase user id (UUID), email address, GitHub numeric id, GitHub login | `users` table, `apps/service/migrations/001_sync.sql` | +| Authentication records | Email address, password hash if you used the email/password path, GitHub OAuth identity, session and refresh tokens | Supabase Auth | +| Pending sign-in | SHA-256 of the device code, the short user code you type, and — while the handshake is open — the session it will hand to your terminal | `device_codes` table, `apps/service/src/store.ts` | +| Project records | Project name, repository as `owner/repo`, your role, who created what, invite codes and who redeemed them | `projects`, `project_members`, `invites` | +| Checkout records | A replica id per checkout, the branch it is on, when it was last seen | `replicas` | +| Presence | Which branch you are on and which paths you touched recently. Held in memory in the websocket gateway, never written to the database | `apps/service/src/ws.ts` | +| Usage events | Seven named events (`user_activated`, `device_authorized`, `project_created`, `invite_created`, `invite_redeemed`, `replica_registered`, `changes_published`), each carrying your Supabase user id, a timestamp, and at most a file-version count and a new-user flag | `apps/service/src/analytics.ts` | +| Website analytics | Four page events with no account id and no persistent identifier attached — see [Cookies and local storage](./cookies.md) | `apps/docs-site/src/analytics.js` | +| Error reports | Route template, HTTP method, status, a platform request id, and a redacted error message | `apps/service/src/observability.ts` | +| Server logs | Standard request logs kept by Vercel and Supabase, which include IP addresses | Vercel and Supabase platform defaults | + +### 3.2 The file contents + +**We hold the contents of the files you sync.** There is no end-to-end encryption. Files +are encrypted in transit (TLS) and at rest under keys the service manages, which means +someone with production access could read them. + +What is stored for each change: the path, the content, the hashes either side merged +against, whether it was a modify or a delete, a timestamp, a size, and a sequence number. + +Only files Git already tracks are ever sent. Untracked files never leave your machine, and +a hard denylist drops `.env*`, `.envrc`, `.npmrc`, `.netrc`, `credentials*`, `secret*`, +SSH private keys, and `*.pem` / `*.key` / `*.p12` / `*.pfx` / `*.jks` / `*.keystore` before +a change is captured — see `packages/core/src/index.ts`. + +### 3.3 Personal data belonging to other people, inside your files + +This is the exposure most policies leave out, so it is stated first-class here. + +Source code routinely contains other people's personal data: names and email addresses in +comments, `git blame` context and `CODEOWNERS`; real email addresses in test fixtures; +customer names, addresses or order records in seed data and database dumps; screenshots and +sample payloads. + +When you sync a repository, that data passes through and is stored by the coordination +service in exactly the same way as the rest of the file content. Neither of us can filter +it — the denylist matches paths, not contents, and nothing inspects what your files say. + +For that data, **you are the controller and Crosscode is your processor.** The +[Data Processing Agreement](./dpa.md) is the terms on which we process it, offered +accept-by-reference so nobody has to negotiate one. + +If you cannot make that commitment for a given repository, do not sync it. Crosscode is +opt-in per checkout and stopping it leaves an ordinary Git repository behind. + +## 4. Categories of data subject + +- **Account holders**: people who sign in and sync. +- **Third parties whose personal data sits inside synced files**: everyone described in + §3.3. They have no relationship with Crosscode and no way to know their data is here, + which is precisely why §3.3 exists. + +## 5. Why we process it, and on what legal basis + +### 5.1 Providing the service — Art. 6(1)(b), contract + +Storing and relaying your file changes, keeping your account and projects, running the +device sign-in handshake, and deciding who may sync with whom. Without this there is no +product to perform. + +### 5.2 Security, abuse prevention and error monitoring — Art. 6(1)(f), legitimate interests + +Rate limiting, invite and device-code expiry, the append-only change log, server logs, and +error reports. + +*Balancing note.* The interest is keeping a service that holds plaintext source code from +being broken into or abused, which is as much your interest as ours. The data used is +narrow: error reports carry a route template, a method, a status, a platform request id and +an error message that has been run through `redact()` — which strips quoted spans, +anything path-shaped, filenames, and long opaque runs — plus stack frames from our own code +by basename only. Request bodies, headers, cookies and environment are never read. Server +logs carry IP addresses, which is unavoidable for any hosted service. Nobody is profiled, +no decision is made about anybody, and none of it is combined with anything else. A user +would expect a service holding their code to watch for intrusions. We consider the interest +to prevail; if you disagree, see §9 on objection. + +### 5.3 Product analytics — Art. 6(1)(f), legitimate interests + +Counting how many people reach the service and whether they get as far as syncing. + +*Balancing note.* Server-side events are keyed on your Supabase user id, carry no file +paths, no content, no branch or repository names, no email and no GitHub login, and are +built from a two-property allowlist rather than spread from an object. Website events carry +no account id and — once the change described in [Cookies and local storage](./cookies.md) +ships — no persistent identifier at all, so they cannot be linked across visits. Nothing is +sold, nothing is shared with advertisers, and no profile is built. The intrusion is close +to nil and the alternative is having no idea whether the project is used at all. We +consider the interest to prevail. + +### 5.4 Legal compliance — Art. 6(1)(c), legal obligation + +Responding to lawful requests, keeping records where the law requires it, and handling +notice-and-takedown. + +### 5.5 Data inside your files — you decide + +For the third-party personal data in §3.3, Crosscode does not choose a legal basis: you do, +as controller. We process it only on your instructions, under the [DPA](./dpa.md). + +## 6. Who receives it + +Vendors we use to run the service, listed with what each one gets, where it sits, and under +what transfer mechanism, on the [Subprocessors](./subprocessors.md) page. That page is the +authoritative list and it changes with 30 days' notice. + +Beyond that: nobody. Your data is not sold, not rented, not shared with advertisers, and +**never used to train an AI model** — ours or anyone else's. Crosscode has no AI features +and stores no model provider credentials. The only agent involved is the one already on +your machine, reading your files locally. + +We disclose data to a public authority only where legally compelled, and will tell you +unless the law forbids it. + +## 7. International transfers + +The provider is a sole proprietor and every vendor is outside the UK/EEA. Details per +vendor are on [Subprocessors](./subprocessors.md); the mechanisms in use are: + +- **Canada** (Supabase's database and auth run in AWS `ca-central-1`, Montréal): covered by + the European Commission's adequacy decision for Canadian commercial organisations, with + Supabase's Standard Contractual Clauses as a belt-and-braces backstop. +- **United States** (Vercel, PostHog, Sentry, GitHub, npm): each vendor's Data Processing + Addendum, incorporating the EU Standard Contractual Clauses; several are additionally + certified under the EU–US Data Privacy Framework. + + + +## 8. How long we keep it + +| Category | Retention | Basis | +| --- | --- | --- | +| File change history | About 7 days, then deleted | `HISTORY_RETENTION_DAYS = 7`, `apps/service/src/store.ts`. The window exists so a checkout that was offline can replay what it missed; one away longer resynchronizes from full content instead. **The sweep that performs the deletion is not yet automated in this repository** — see the note below. | +| Account record, projects, memberships | Until you ask us to delete them | Needed to perform the contract | +| Supabase Auth records (email, password hash, GitHub identity, sessions) | Until account deletion; refresh tokens expire or are revoked on sign-out | Needed to perform the contract | +| Device sign-in codes | About 15 minutes, and consumed on first successful poll. Rows older than an hour are deleted at the start of the next handshake; the session payload is nulled the moment the terminal collects it | `apps/service/src/store.ts` | +| Invite codes | Until they expire or are redeemed; the row survives to record that it was used | `invites` table | +| Replica records | Until the project is deleted | `replicas` table | +| Presence | In memory only. Gone when the connection closes or the process restarts | `apps/service/src/ws.ts` | +| Server-side analytics events | PostHog's project retention setting, currently the plan default | PostHog | +| Website analytics events | As above | PostHog | +| Error reports | Sentry's project retention setting, currently the plan default (90 days on Sentry's standard plans) | Sentry | +| Server and platform logs | Vercel's and Supabase's own log retention, which is days, not months, on their current plans | Vercel, Supabase | +| Database backups | Supabase's automated backups, on the plan's schedule. Deleted data survives in a backup until that backup ages out | Supabase | + +**Honest note on the 7 days.** The service enforces the window on the way *out*: a cursor +pointing at history older than what survives is refused rather than answered with a partial +page, and the daemon resynchronizes from full content. The deletion job itself is operated +outside this repository — there is no cron entry or scheduled function in the source tree +that performs it. Treat "about 7 days" as the retention *target* and the serving guarantee, +not as a verified automated delete. + + + +## 9. Your rights + +Under the UK/EU GDPR you can ask for: + +- **Access** — a copy of the personal data we hold about you. +- **Rectification** — correction of anything inaccurate. +- **Erasure** — deletion of your account, your projects, and the change history attached to + them. Your repositories are unaffected, because they were never ours. +- **Restriction** — that we stop processing while a dispute is resolved. +- **Portability** — the data you gave us, in a machine-readable form. +- **Objection** — to anything done on legitimate interests (§5.2, §5.3). Say so and we + stop, unless we can show compelling grounds that override your interests. +- **Withdrawal of consent** — where we ever rely on consent. Today we do not; if that + changes, withdrawing is as easy as giving it and does not affect what came before. + +**How to exercise them:** email **privacy@getcrosscode.dev**. We respond within one month +of receipt, extendable by two further months for genuinely complex requests, in which case +we will tell you inside the first month and say why. + +Requests are free. We may ask you to prove you control the account before acting on one — +handing someone else's source code to whoever asked is the failure mode a policy like this +exists to prevent. + +**One limit, stated plainly.** Removing a member from a project ends their access +immediately, but it cannot un-share what they already have: they had a full checkout of the +repository. No product can reach into a copy someone already holds. + +### If you are one of the third parties in §3.3 + +Your data reached us inside someone else's repository. We usually cannot identify you, and +searching stored file contents to find you would mean reading them, which is worse than the +problem. Ask the person or team that syncs the repository — they are the controller. If you +tell us who they are at **privacy@getcrosscode.dev**, we will pass the request on. + +### Complaints + +Tell us first if you are willing — **privacy@getcrosscode.dev** — but you do not have to. +You can complain to a supervisory authority in the EU/EEA member state where you live, +work, or where you think something went wrong. In the UK that is the Information +Commissioner's Office (`ico.org.uk`). + +## 10. Automated decision-making + +There is none. No automated decision produces legal or similarly significant effects about +anybody, and there is no profiling. Nothing in the service scores, ranks, or classifies +users. The analytics events in §3.1 are counted in aggregate and are not used to make +decisions about individuals. + +## 11. Age + +You must be at least 16 to use Crosscode, or the age of digital consent in your country if +that is higher. This matches §3 of the [Terms of Service](./terms.md). The service is not +directed at children and collects no data knowingly from them. If you believe a child's +data is here, write to **privacy@getcrosscode.dev** and it will be deleted. + +## 12. Is any of this required of you? + +Providing an email address and a GitHub identity is a condition of having an account — +without them there is nobody to authorise and no way to check repository access at invite +redemption. Not providing them means not using the hosted service. The software still works +locally. + +Syncing any given repository is entirely your choice, per checkout. + +## 13. Changes to this policy + +Every version of this page is in the repository's Git history, so what changed and when is +public and checkable. + +For material changes — a new purpose, a new legal basis, a new category of data — we will +give at least **30 days' notice** by email to account holders and by a dated note at the top +of this page, before the change takes effect. Corrections and clarifications go live +immediately and are visible in the Git history. + +## Before this takes effect + +Fill in, in this file: + +- `{{PROVIDER_NAME}}` — the legal name of the individual provider +- `{{PROVIDER_ADDRESS}}` — a postal address for legal notices +- `{{EFFECTIVE_DATE}}` — the date this policy takes effect + +And resolve, before publishing: + +- The retention-sweep gap in §8. Either automate the delete or soften the claim. +- The two `` notes in §1 and §7. +- Confirm the actual retention settings configured in the PostHog and Sentry projects and + replace "the plan default" in §8 with the number. diff --git a/docs/privacy.md b/docs/privacy.md index 1a8b587..e12e7ee 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -3,6 +3,12 @@ Crosscode syncs your uncommitted edits between checkouts, so those files pass through our coordination service. This page says exactly what that service holds. +This is the honest summary. The complete, formal notice — legal bases, retention per +category, your rights and how to exercise them — is the +[Privacy Policy](./privacy-policy.md). Alongside it: [Cookies and local +storage](./cookies.md), [Subprocessors](./subprocessors.md), and the [Data Processing +Agreement](./dpa.md) for personal data that sits inside the files you sync. + **We hold your file contents.** There is no end-to-end encryption. Files are encrypted in transit (TLS) and at rest, under keys we manage, which means a Crosscode engineer with production access could read them. We would rather say that plainly than imply otherwise. @@ -64,3 +70,7 @@ tell you that than let you believe otherwise. Ask us and we delete your account, your projects, and the change history attached to them. See [support](./support.md). Your repositories are unaffected, because they were never ours. + +Your other rights — access, rectification, restriction, portability, objection — and the +one-month response time are in §9 of the [Privacy Policy](./privacy-policy.md). Write to +privacy@getcrosscode.dev for any of them. diff --git a/docs/security.md b/docs/security.md index 6806c42..217a43d 100644 --- a/docs/security.md +++ b/docs/security.md @@ -139,3 +139,210 @@ large your changes are. Report vulnerabilities per [SECURITY.md](https://github.com/amsultan2010/crosscode/blob/main/SECURITY.md), never in a public issue. + +## Breach response runbook + +Crosscode stores plaintext customer source code. A breach here is severe by definition — +there is no "only metadata was exposed" outcome available. Assume the worst category until +the evidence rules it out. + +The GDPR clock is **72 hours** to notify a supervisory authority, and Crosscode's +[DPA](./dpa.md) promises controllers notice within **48 hours**. Both clocks start at +*awareness*, not at containment. + +**Who decides: the project owner.** One person, named in the +[privacy policy](./privacy-policy.md), owns every call below — severity, notification, +disclosure. There is no committee to convene and no escalation path. If the owner is +unreachable, the breach waits, which is a real risk of a one-person project and is stated +rather than papered over. + +### 1. Detection + +Where a breach realistically surfaces: + +- **Sentry** — a spike in 5xx, an unfamiliar error type, or auth errors from a route that + should not produce them. +- **The uptime workflow** (`.github/workflows/uptime.yml`) — opens a GitHub issue on two + consecutive failed probes. An outage is not a breach, but they arrive together often + enough that this is a trigger to look. +- **A user or researcher emailing `security@getcrosscode.dev`**, per + [SECURITY.md](https://github.com/amsultan2010/crosscode/blob/main/SECURITY.md). This is + the most likely source. Treat it as credible until disproven. +- **Vercel or Supabase telling you** — a provider security notice, or an unrecognised login + to either dashboard. +- **Anomalies you notice yourself**: unexpected rows in `device_codes`, invites redeemed by + accounts you do not recognise, egress or database size that does not match usage. + +**Awareness starts** when you have a reasonable degree of certainty that a security +incident occurred and it involved personal data. Not when you finish investigating. Write +the timestamp down the moment you reach it — the 72 hours is measured from there and you +will be asked to evidence it. + +### 2. First 60 minutes — contain, then preserve + +In this order: + +1. **Stop the bleeding.** Rotate what is exposed: Supabase service role key and database + password, `SENTRY_DSN`, `POSTHOG_KEY`, and any GitHub OAuth app secret. If a deployment + is the cause, promote the last known good one from the Vercel dashboard — a promotion + takes seconds and a diagnosis does not. +2. **Do not delete anything.** Not logs, not rows, not the bad deployment. Evidence first; + see step 3. +3. **Revoke sessions** if account takeover is plausible — Supabase Auth, sign out all users + on the affected accounts. +4. **Write down the clock**: when it started, and how you know. + +### 3. Preserve evidence + +Before anything ages out. Vercel and Supabase log retention is days, not months, so this is +genuinely urgent: + +- Export Vercel function logs for the window (**Vercel → project → Logs**, filter and + export). +- Export Supabase Postgres logs and the auth audit log for the window. +- Export the Sentry issues involved, including the full event JSON, not just the title. +- Snapshot the database if data was modified — Supabase's dashboard can take one on demand. +- Note the deployment SHA in production at the time (`VERCEL_GIT_COMMIT_SHA`). +- Keep the original report email intact, headers included. + +Put all of it in one dated folder outside the repository. Do not commit evidence containing +personal data to a public repo. + +### 4. Assess severity + +| Level | What it means | Examples | +| --- | --- | --- | +| **P1 — file contents exposed** | Someone who should not have been able to read stored file contents did, or plausibly could have | Database credential leak, RLS bypass, a route serving another project's changes, backup left readable | +| **P2 — account or auth compromise** | Identity or session data exposed, without confirmed content access | Device-code or session token leak, OAuth secret exposure, account takeover | +| **P3 — metadata exposure** | Records about accounts and projects, no file contents | Email addresses, GitHub logins, `owner/repo` names, project membership | +| **P4 — no personal data** | A security issue with no personal data involved | A vulnerability reported and fixed before exploitation, with logs showing no access | + +**P1 and P2 are notifiable.** P3 usually is — repository names alone can be commercially +sensitive, and the honest reading is that `owner/repo` plus membership tells an attacker who +works on what. P4 is not notifiable, but write it up anyway. + +The three questions that decide it: *Was personal data involved? Could someone unauthorised +have accessed it? Is there a risk to those people's rights and freedoms?* If you cannot +answer the second one "no" with evidence, treat it as yes. + +### 5. Notify + +**Supervisory authority — within 72 hours of awareness.** Late is better than never: a late +notification must explain the delay, so file within the window even if the picture is +incomplete. Art. 33(4) explicitly allows notifying in phases. + +```text +Subject: Personal data breach notification — Crosscode + +1. Nature of the breach + What happened, in two sentences. When it started, when it was discovered, whether it is + contained now. + +2. Categories and approximate number of data subjects + Account holders affected: N. + Third parties whose personal data was inside the affected repositories: unknown — + Crosscode does not inspect file contents and cannot enumerate them. See the privacy + policy, §3.3. + +3. Categories and approximate number of records + Which of: file contents and paths; account records (email, GitHub identity); + authentication records; project and membership records; analytics or error reports. + Number of file versions, projects and accounts, as best established. + +4. Likely consequences + For a P1: source code, and any personal data inside it, was readable by an unauthorised + party. Downstream risk to the affected users' own customers, which those users must + assess as controllers. + +5. Measures taken and proposed + Containment already done, credentials rotated, fix deployed, notifications sent, changes + to prevent recurrence. + +6. Contact + {{PROVIDER_NAME}}, privacy@getcrosscode.dev + +7. What is not yet known + State it explicitly, with the date by which you expect to know. +``` + +**Affected users — without undue delay, where there is high risk to their rights and +freedoms.** For a P1, assume there is. Send from `security@getcrosscode.dev`: + +```text +Subject: Security incident affecting your Crosscode data — action needed + +What happened + On . We discovered it on + at