# Privacy and security

## Controls

| Control | Implementation |
|---|---|
| Transport | Cloud Run terminates TLS; HSTS and security headers via Helmet |
| CORS | Strict allowlist, no wildcard; required non-empty in production |
| Request size | `REQUEST_BODY_LIMIT`, default 1 MB |
| Rate limiting | Per-window cap; `trust proxy` is set to exactly one hop so a client cannot spoof its address |
| Schema validation | Zod on every body, param and query before any repository call |
| Authentication | OIDC with signature, issuer, audience, expiry, subject and scope validation |
| Authorization | Evaluated per resource on every protected request; deny by default |
| Object keys | Every component validated against the opaque-identifier pattern; traversal, absolute paths and email-derived keys impossible by construction |
| Public objects | None; uniform bucket-level access |
| Signed URLs | Short-lived (`SIGNED_URL_EXPIRATION_MINUTES`), issued only after an ownership check, and never treated as authorization |
| Secrets | Google Secret Manager; `sm://…` values resolved at startup |
| Logging | Structured, with a redactor that removes text, prompts, completions, credentials, tokens and embeddings |
| Pseudonymisation | HMAC-derived participant pseudonyms, stable per thread |
| Service accounts | Separate least-privilege identities for API, worker and tasks |
| Audit | Content access, research access, exports, grants, revocations and deletions — including denials |
| Retention | `STORE_RAW_TEXT`, `STORE_SEGMENT_TEXT`, `STORE_EMBEDDINGS`, `ENABLE_THREAD_DELETION`, plus bucket lifecycle rules |
| Encryption | Google-managed by default; optional CMEK through `GCS_KMS_KEY` |

## Identifiers

`ownerId` and `ownerKey` are HMAC derivations of the authenticated subject under
`OWNER_KEY_SALT`. **No object key ever contains an email address, a JWT subject
or any client-supplied string.** The key builder rejects an owner key that does
not carry the derived `ok_` prefix, so an email cannot reach a path even through
a coding error.

## What is logged

Structured JSON to stdout/stderr for Cloud Logging: method, route, status,
duration, request id, and audit records. The redactor strips any field whose
name matches text, content, prompt, completion, key, authorization, password,
secret, token, credential, embedding or vector. Token *counts* are allowed
through by an explicit allowlist; token *values* never are.

Raw conversation text never appears in operational logs.

## Privacy-preserving storage

With `STORE_RAW_TEXT=false` contributions are stored without text. The content
hash, structural measures, provenance and analysis status are retained, so
Layer 1 aggregates still work while text-dependent measures return `null` with
`INSUFFICIENT_TEXT`.

With `STORE_EMBEDDINGS=false` semantic artifacts are stored without vectors.

`STORE_SEGMENT_TEXT=false` (the default) also strips semantic unit text at the
repository boundary. These switches affect new writes, not historical objects,
exports, caches, backups, or material already delivered to an LRS. Raw text and
embeddings remain enabled by default; choose their retention policy explicitly.

Administrator content access is disabled by default. Explicitly opting in with
`ADMIN_CONTENT_ACCESS=true` enables audited cross-owner console reads; see the
[authorization matrix](authorization-matrix.md). New contribution xAPI statements
omit participant display labels, and thread initialization omits the private title.
Pseudonymisation is not anonymisation.

## Deletion

`DELETE /threads/:threadId` respects `ENABLE_THREAD_DELETION`. When disabled the
thread is marked `deletion_requested`, the request is audited, and the response
is `202` explaining the retention constraint — the request is recorded rather
than silently ignored. When enabled the GCS adapter enumerates live and noncurrent
generations beneath the thread prefix and updates the owner index after deletion.
Retention holds, bucket soft-delete policies, separately stored exports and LRS
records require an explicit lifecycle policy. This endpoint is not a guarantee of
immediate, irreversible erasure everywhere.

## Threats considered

| Threat | Mitigation |
|---|---|
| Path traversal through identifiers | Every key component validated; `..`, `//` and leading `/` rejected |
| Cross-owner access | Owner key derived from the token, never from the request |
| Researcher scope escalation | Scopes read from the stored grant only; responses projected field by field |
| Stale authorization after revocation | Every request re-reads the grant; no caching |
| Administrative overreach | The administrator role confers no content access |
| Task endpoint forgery | OIDC validation of issuer, audience and service-account identity |
| Replay / duplicate ingestion | Generation preconditions plus deterministic job identifiers |
| Secret leakage through readiness | `/ready` reports driver names and booleans only |
| Log-based leakage | Redaction by field-name pattern |
| Inference from absent fields | Ungranted fields are omitted, not nulled |

## Known limitations

- `pseudonymized_text` masks emails, phone numbers and URLs. It is a mitigation,
  not de-identification: conversational content can be re-identifying by itself.
- Version 1 assumes one primary human data owner per owner-scoped thread. Other
  humans in a multi-party thread remain data subjects, but their rights are
  policy-defined rather than enforced by a second ownership key.
- Embeddings are sensitive derived data. They are separately scoped, but an
  embedding of a short contribution can carry substantial information about it.
- The evaluation harness measures agreement with human annotation, not validity.
