> ## Documentation Index
> Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Protection

> PII encryption, submission data isolation, audit logging, and log safety

# Data Protection

> Sensitive data is encrypted at rest, isolated per user, and never appears in logs.

***

## PII Encryption

Bank account numbers and SSNs are encrypted with AES before storage. Each value gets a random IV — the same plaintext produces different ciphertext every time:

```typescript theme={null}
// src/lib/encryption.ts
export function encryptPII(value: string): string {
  const iv = randomBytes(16);
  const cipher = createCipheriv(ALGORITHM, KEY, iv);
  // ...
  return `enc:v1:${iv}:${ciphertext}`; // Prefix identifies encrypted values
}
```

During form submission, only bank-type fields are encrypted — other fields pass through unchanged:

```typescript theme={null}
// Before saving
const encrypted = encryptSubmissionPII(data, schema);
// → { name: "Jane Doe", bank_field: "enc:v1:a3b7..." }
```

Confirmation fields (`email_confirm`, `confirmAccountNumber`) are stripped entirely — they exist only for UI validation and are never stored.

**Tests:** 32 cases in `src/lib/__tests__/encryption.test.ts`

```bash theme={null}
pnpm --dir apps/terra test -- -t "Encryption Module"
```

<Accordion title="What the tests prove">
  * Round-trip encryption/decryption works for strings, special chars, and
    unicode - Random IV produces different ciphertext for same plaintext - Plain
    values pass through decryption safely (no double-encrypt risk) - Bank data:
    `accountNumber`/`routingNumber` encrypted, `bankName` stays plain -
    `confirmAccountNumber` stripped from stored data - Submission-level: only bank
    fields encrypted, everything else untouched - Malformed ciphertext throws a
    clear error (not garbage data)
</Accordion>

**Try to break it:** After submitting a form with bank fields, query the `submissions` table directly:

```sql theme={null}
-- Bank fields should show enc:v1:... not plaintext
SELECT id, data FROM submissions
WHERE data::text LIKE '%accountNumber%'
ORDER BY created_at DESC LIMIT 5;
```

***

## Submission Data Isolation

```mermaid theme={null}
flowchart TD
    subgraph Authenticated["Authenticated Form"]
        Check["submission.user_id === session.user.id"]
        Check -->|match| Allow[Access granted]
        Check -->|mismatch| Deny["Unauthorized access" + audit log]
    end

    subgraph Anonymous["Anonymous Form"]
        NoCheck[No ownership check — by design]
    end

    subgraph StatusLookup["Status Lookup (Public)"]
        Safe[Returns ONLY status + form name]
        Safe -.->|no PII| NoPII[No email, phone, or submission data]
    end
```

```typescript theme={null}
// src/lib/security/submission-guard.ts
export async function verifySubmissionAccess(submissionId: string) {
  const submission = await fetchSubmission(submissionId);

  if (!submission.form.is_anonymous) {
    // Authenticated form: verify ownership
    if (submission.user_id !== session.user.id) {
      return { success: false, error: "Unauthorized access" };
    }
  }
  // Anonymous forms: no check (intentional)
}
```

**Tests:** 11 cases in `plaid-security.test.ts`, 16 in `status.test.ts`

```bash theme={null}
pnpm --dir apps/terra test -- -t "Plaid Security"
pnpm --dir apps/terra test -- -t "Status Lookup Security"
```

<Accordion title="What the tests prove">
  * Owner can access their own submission data - Cross-user access blocked (User
    A cannot see User B's bank details) - Unauthenticated users blocked on
    authenticated forms - Anonymous forms work without session (by design) -
    Status lookup returns only status + form name — no PII fields - Only 8-char
    reference ID exposed, not full UUID
</Accordion>

***

## Audit Logging

Every significant action creates an immutable audit record for SOC 2 compliance:

```typescript theme={null}
// src/lib/audit-logger.ts
await auditLog({
  action_type: "update",
  entity_type: "submission",
  entity_id: submissionId,
  user_id: session.user.id,
  changes: {
    status: { before: "submitted", after: "approved" },
  },
});
```

| What's logged                      | What's NOT logged          |
| ---------------------------------- | -------------------------- |
| User IDs, entity IDs, action types | Email addresses            |
| Before/after values for changes    | Phone numbers              |
| Anonymized IP (`192.168.1.0`)      | Raw submission data        |
| Timestamps, request IDs            | SSNs, bank account numbers |

**Tests:** 29 cases in `src/lib/__tests__/audit.test.ts`

```bash theme={null}
pnpm --dir apps/terra test -- -t "Audit Module"
```

<Accordion title="What the tests prove">
  * IP anonymized: last octet zeroed for IPv4, prefix-only for IPv6 - Missing
    session logs null user fields (no crash) - Before/after changes stored
    correctly - Failed actions logged with `success: false` and error message -
    All action types covered: form CRUD, publish, team invite/remove, status
    change, external sync - Non-blocking: audit failures don't block the original
    action
</Accordion>

**Try to break it:** Perform a sequence of actions (create form → publish → invite member → submit → change status) and query `audit_logs`. Every action should have an entry. Search for email patterns — there should be none.

***

## Log Safety

All logging goes through a structured logger with PII redaction:

```typescript theme={null}
// src/lib/logger.ts — automatic redaction
logger.info("User action", { email: "jane@example.com" });
// Output: { email: "j***e@example.com" }

logger.info("SMS sent", { phone: "555-123-4567" });
// Output: { phone: "***4567" }
```

| Data type | Redacted output     |
| --------- | ------------------- |
| Email     | `j***e@example.com` |
| Phone     | `***4567`           |
| User ID   | `abc123de***`       |
| API key   | `sk_live_***`       |

`console.log` is banned by ESLint — the linter catches it:

```bash theme={null}
pnpm --dir apps/terra lint
```

***

<CardGroup cols={2}>
  <Card title="Functional Logic" icon="gears" href="/review/functional-logic">
    Core workflows and integrations
  </Card>

  <Card title="Known Gaps" icon="triangle-exclamation" href="/review/known-gaps">
    What's missing and the plan
  </Card>
</CardGroup>
