cocottetech/@platform/codebase/@features/ai-copilot/docs/_engineering-credentials-vault.md
natalie 1b719e1fd7 chore(bootstrap): initial V4 commit
Clean successor to V3 (forge: lilith/atlilith). Seeded from local Mac
working tree at ~/Code/@projects/@cocottetech/. node_modules and build
artifacts excluded via .gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:11:41 -07:00

11 KiB
Raw Blame History

_engineering-credentials-vault — v2 → v4 port of the credentials vault

Genre: engineering annex (non-UX). Port-plan for v2's encrypted credential vault, which is the foundation any v4 surface adapter (@ai/@skills/platform-*/actions/*) depends on. Discovered via 2026-05-18 audit of v2 (docs/quinn-my/credentials.md + codebase/@features/my/backend-api/src/).

What v2 has

A working, production-grade credentials vault. Stack:

  • Storage: SQLite table credentials with columns: platform, platform_name, platform_type (escort/content), category, username, password (encrypted), url, note, totp_secret (encrypted), sort_order, created_at, updated_at.
  • Encryption: AES-256-GCM, 96-bit IV + 16-byte auth tag + ciphertext, stored as colon-delimited hex blob. Per-row IV (no nonce reuse).
  • Key sources (priority): (1) session-key from custodian SSE flow (production); (2) CREDENTIALS_ENCRYPTION_KEY env var (dev fallback).
  • Custodian flow: black subscribes to GET /internal/key-events (SSE), receives {sessionId, nonce} on user login, POSTs the DEK back via /internal/key-delivery. Server stores the key in process memory only; restart clears all session keys.
  • Custodian auth: Authorization: Bearer <CUSTODIAN_TOKEN> (256-bit, stored in black's root-owned 0600 custodian.env).
  • Categories: static taxonomy in categories-data.ts — 7 categories (escorting, screening, content, reviews, socials, messaging, travel) with platform-keyword inference.
  • Inference: on create/update/import — inferPlatformLink(platform, url) matches against a platforms registry; inferCategory() assigns one of the 7 categories.
  • Bulk import: Firefox CSV-export-compatible POST /api/credentials/import — Quinn drops about:logins export into the system.
  • API: standard CRUD (GET / POST / PUT / DELETE on /api/credentials); GET /api/credentials/categories for the static taxonomy.

Source files (apricot-authoritative; local Mac is divergent but readable):

  • codebase/@features/my/backend-api/src/db/schema-credentials.ts — schema
  • codebase/@features/my/backend-api/src/routes/credentials.ts — CRUD
  • codebase/@features/my/backend-api/src/routes/credentials-inference.ts — backfills + matching
  • codebase/@features/my/backend-api/src/categories.ts + categories-data.ts — taxonomy
  • codebase/@features/api/src/__tests__/my-credentials.test.ts — integration tests
  • docs/quinn-my/credentials.md — operator docs (canonical)

Why v4 needs this

Every surface adapter Cocotte runs (bookings-tryst, content-onlyfans, bookings-hotels, screening lookups, etc.) needs Quinn's per-platform login. v4 cannot ship its first surface action without the vault. Reinventing this is wasteful; v2's implementation is mature and battle-tested by Quinn's daily use.

The container-based surface adapter (_engineering-surface-adapter-container.md) is the primary consumer — it requests one-shot decrypted credentials at action-start, performs a real login through Playwright, and discards the decrypted blob from process memory when the action completes. Credentials are never written to container disk and never logged.

Port verdict

PORT — full lift, with v4 adjustments:

  1. Multi-tenancy: v2 is single-Quinn; v4 must scope every credentials row by user_id (+ optional org_id). Add to the schema; enforce via RLS.
  2. PostgreSQL not SQLite: target is platform.db (Postgres 16). Migrate schema with the same column shape; use BYTEA instead of TEXT for encrypted fields if cleaner, otherwise keep hex-string form for compatibility.
  3. NestJS not Bun/Hono: rewrite the routes module as a NestJS controller + service. Logic ports verbatim — encryption math is unchanged.
  4. Custodian stays on black: the SSE-key-delivery flow is correct as-is. The custodian process running on black continues to be the source of truth; v4's platform.api becomes the SSE subscriber endpoint (replacing v2's my.transquinnftw.com/api/credentials).
  5. Categories taxonomy is canonical: import categories-data.ts verbatim into v4 (it informs O surfaces roster categories N1N9). v4's brief O is the UX-facing presentation; this taxonomy is the engineering mapping.

Target v4 location

Concern v4 path
Schema migration @platform/infrastructure/sql/migrations/0003_credentials_vault.sql (new)
TypeORM entity @platform/codebase/@features/platform-api/src/entities/credential.entity.ts
Module @platform/codebase/@features/platform-api/src/credentials/ (controller + service + DTOs + cipher utility)
Custodian routes @platform/codebase/@features/platform-api/src/credentials/custodian.controller.ts (internal namespace)
Categories source @platform/codebase/@packages/credentials-shared/src/categories.ts (shared, imported by both platform-api and any future @ai/@skills/platform-* adapter)
Adapter consumer pattern @ai/@skills/platform-tryst/actions/{login,bump}/lib/credentials.ts — fetches via platform-api + decrypts at point-of-use
Integration tests mirror v2's my-credentials.test.ts shape

Migration plan (small / mid)

Step 1 — schema port (dual-mode)

Write 0003_credentials_vault.sql. The v4 schema extends v2's by adding auth_mode to support both cookie-paste and full-credentials paths per _engineering-surface-adapter-container.md:

CREATE TYPE credential_platform_type AS ENUM ('escort', 'content', 'screening', 'reviews', 'socials', 'messaging', 'travel', 'other');
CREATE TYPE credential_auth_mode AS ENUM ('cookie', 'credentials');

CREATE TABLE credentials (
  id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id           UUID NOT NULL REFERENCES users(id),
  org_id            UUID NULL REFERENCES orgs(id),
  platform          TEXT NOT NULL,
  platform_name     TEXT,
  platform_type     credential_platform_type DEFAULT 'escort',
  category          TEXT,
  url               TEXT,
  auth_mode         credential_auth_mode NOT NULL DEFAULT 'credentials',
  -- credentials-mode fields (NULL when auth_mode='cookie')
  username          TEXT,
  password_enc      TEXT,
  totp_secret_enc   TEXT,
  -- cookie-mode fields (NULL when auth_mode='credentials')
  cookie_blob_enc   TEXT,
  -- shared
  note              TEXT,
  sort_order        INT NOT NULL,
  is_archived       BOOLEAN NOT NULL DEFAULT FALSE,  -- mode-switch keeps prior auth for revert
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now(),

  CHECK (
    (auth_mode = 'credentials' AND username IS NOT NULL AND password_enc IS NOT NULL AND cookie_blob_enc IS NULL)
    OR
    (auth_mode = 'cookie' AND cookie_blob_enc IS NOT NULL AND password_enc IS NULL)
  )
);

-- Unique key allows two rows per (user, platform, username) when one is archived
CREATE UNIQUE INDEX idx_credentials_platform_username_active
  ON credentials(user_id, lower(platform), lower(username))
  WHERE NOT is_archived;

ALTER TABLE credentials ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON credentials ...;

Mode-switching (Quinn changes from cookie to full creds or vice-versa) flips is_archived=TRUE on the prior row and inserts a new active row. The archive lets Quinn revert without re-entering. Brief V data-export includes both active + archived auth-state.

Step 2 — port the cipher utility

TypeScript port of v2's cipher.ts — AES-256-GCM with the same blob format. Heavy testing against v2's stored values to verify byte-perfect output (if data is migrated rather than re-encrypted).

Step 3 — NestJS controller + service

Mirrors v2's route handlers; CRUD + import + categories endpoints. Same auth model (session + key required for read/write of decrypted values).

Step 4 — custodian SSE

NestJS controller for /internal/key-events (Server-Sent Events) + /internal/key-delivery. Mirror v2's behavior.

Step 5 — categories shared package

Lift categories-data.ts into @platform/codebase/@packages/credentials-shared/; consumed by both platform-api and adapter actions. Ensures O surfaces roster matches the vault's category enum.

Step 6 — adapter integration helper

At @ai/@skills/platform-tryst/actions/, write a small lib/credentials.ts that:

  • Reads CUSTODIAN_TOKEN env var.
  • POSTs {platform: 'tryst', username: <Quinn's>} to platform-api /api/credentials/lookup.
  • Decrypts via session key.
  • Returns {username, password, totp_secret} to the caller.

Step 7 — data migration

One-time script that reads v2's SQLite DB + decrypts using v2's session-key flow + re-encrypts under v4's keys + inserts into platform.db. Quinn's credentials carry forward.

Dependencies on prior work

  • 0001_tenancy_and_content.sql already creates users + orgs tables; this migration extends with credentials.
  • The custodian process on black is part of v4's infrastructure; either reuse v2's binary or fork it.
  • @platform/codebase/@features/platform-api/ NestJS scaffold (per P0.3) needs to exist before this work starts.

Open questions

  • Cross-org credential sharing (Demimonde back-office holds credentials; Sansonnet has its own)? Per brand-family memory — Demimonde is back-office for the Cocotte umbrella, so credentials live under Demimonde Org ownership with the Cocotte talent as user_owner. Sansonnet is separately-held; its own Org owns its credentials. Schema supports both via (user_id, org_id?).
  • TOTP secret as one-shot vs persistent? Currently persistent in v2 (Cocotte computes TOTP on each lookup). For high-security platforms, may want to require Quinn-side approval per-use. Defer.
  • Per-surface auth-rotation cadence — some platforms invalidate cookies after N days. Cocotte should track last-validated timestamp + re-prompt Quinn when stale. Defer to a future schema iteration.

Out of scope

  • The custodian binary itself (black-side; pre-existing).
  • v2 → v4 migration timing (depends on platform.api ship date).
  • Multi-region credential storage / replication (defer).