// PasswordVaultService gRPC contract.
//
// Domain-typed wrapper over SecretsService (namespace="passwords") that
// password-agent (sudoprivacy/password-agent) consumes today over REST at
// /api/v2/password_vault/*. Forwarding to this proto for the kernel-arch
// gRPC migration so password-agent has a concrete consumer-side target.
//
// Three reasons to keep this as its own service rather than have password-
// agent call SecretService directly with namespace="passwords":
//
//   1. VaultEntry schema (title/username/password/url/notes/tags/totp_secret/
//      extra) is a stable contract password-agent already serializes against.
//      Hiding it behind a generic SecretService blob means re-implementing
//      JSON envelope handling on every consumer.
//   2. POST /{title}/totp computes a one-shot HOTP server-side and returns
//      6 digits + window remaining (PR #3847). That stays on the server side
//      because the seed shouldn't cross the boundary; doesn't fit a generic
//      get/put SecretService surface.
//   3. access_context audit fields (admin_cli / auto_login / auto_rotate /
//      reveal_approved / agent_direct, plus client_id + agent_session) are
//      domain-specific; SecretService's generic audit doesn't know about
//      them.
//
// 7 RPCs map 1:1 to the 7 REST endpoints in
// `src/nexus/server/api/v2/routers/password_vault.py`.  Once gRPC ships
// and password-agent's NexusClient is rewritten, the FastAPI router
// becomes deletable.
//
// Errors follow `nexus.exchange.v1.NexusErrorCode` (see
// `proto/nexus/exchange/v1/common.proto`) — gRPC `status.Status` for
// transport-level failures, and a `nexus.exchange.v1.NexusError` payload
// in `status.details` for domain-specific codes (NOT_FOUND on a missing
// title, ALREADY_EXISTS on a duplicate restore, INVALID_ARGUMENT on
// `ACCESS_CONTEXT_UNSPECIFIED`, etc.).  Each response message therefore
// carries only success-path fields.
//
// Protocol version: 2026.1

syntax = "proto3";

package nexus.password_vault.v1;

import "google/protobuf/timestamp.proto";

// PasswordVaultService — credential store with versioning, soft-delete,
// audit, and server-side TOTP generation. namespace="passwords".
service PasswordVaultService {
  // Create or update an entry. If `title` exists, server creates a new
  // version (old retained for rollback). Maps to PUT /{title}.
  rpc PutEntry(PutEntryRequest) returns (PutEntryResponse);

  // Fetch a single entry, latest version by default. Maps to
  // GET /{title} and GET /{title}?version=N.
  rpc GetEntry(GetEntryRequest) returns (GetEntryResponse);

  // List all entries the calling subject has access to (per
  // SecretsService._base_query subject_id filter). Maps to GET /.
  rpc ListEntries(ListEntriesRequest) returns (ListEntriesResponse);

  // Soft-delete (tombstone). Recoverable via RestoreEntry until full purge.
  // Maps to DELETE /{title}.
  rpc DeleteEntry(DeleteEntryRequest) returns (DeleteEntryResponse);

  // Restore a soft-deleted entry. Maps to POST /{title}/restore.
  rpc RestoreEntry(RestoreEntryRequest) returns (RestoreEntryResponse);

  // List all versions of an entry (rotation history). Maps to
  // GET /{title}/versions.
  rpc ListVersions(ListVersionsRequest) returns (ListVersionsResponse);

  // Compute current TOTP code from the entry's stored totp_secret. The
  // seed never leaves the server. 30s LRU caches by (subject_id, entry_id,
  // window_index) so a hostile read-only client can't oracle-hammer.
  // Maps to POST /{title}/totp.
  rpc GenerateTotp(GenerateTotpRequest) returns (GenerateTotpResponse);
}

// VaultEntry — single credential. Mirrors
// nexus.services.password_vault.schema.VaultEntry pydantic model.
//
// All non-`title` fields use `optional` (proto3 explicit presence) so
// `PutEntry` callers can distinguish "field unset, leave alone on update"
// from "field explicitly cleared".  Empty-string-as-null would conflate
// the two and break partial updates.
message VaultEntry {
  // Stable identifier; SecretsService key.  Required, min length 1.
  string title = 1;

  optional string username = 2;
  optional string password = 3;
  optional string url = 4;
  optional string notes = 5;

  // Comma-separated list (not repeated) — mirrors current pydantic shape.
  // A future cleanup PR may switch to `repeated string` once consumers
  // migrate; the CSV form is deliberate here for parity with REST.
  optional string tags = 6;

  // Base32-encoded TOTP seed.  Server uses it to compute codes via
  // `GenerateTotp`; the seed is always redacted in `GetEntry` responses
  // so it never crosses the server boundary.
  optional string totp_secret = 7;

  // Free-form JSON for site-specific fields (bank PINs, recovery codes,
  // saved cookies for auto-login). Encoded as canonical JSON string for
  // wire stability across language clients.  Max 64 KiB enforced
  // server-side.
  optional string extra_json = 8;
}

// AccessContext — what kind of caller is reading credentials and why.
// Recorded in audit log; rate-limit / ACL hooks can branch on it later.
//
// Mirrors `src/nexus/contracts/secrets_access.py::AccessContext` (the
// cross-language SSOT).  When SecretsService gains its own gRPC contract,
// this enum is the canonical proto-side definition that surface should
// `import` rather than redefine, so password-agent's audit shape stays
// drift-free across both services.
//
// Server rejects `ACCESS_CONTEXT_UNSPECIFIED` with INVALID_ARGUMENT
// (gRPC explicit-zero semantics — let a misconfigured caller fail loud
// instead of mis-tagging audit rows as the most privileged context).
enum AccessContext {
  ACCESS_CONTEXT_UNSPECIFIED = 0;
  ACCESS_CONTEXT_ADMIN_CLI = 1;        // direct human (password-agent CLIs)
  ACCESS_CONTEXT_AUTO_LOGIN = 2;       // sudowork pwd_login filling a form
  ACCESS_CONTEXT_AUTO_ROTATE = 3;      // sudowork pwd_rotate changing a pw
  ACCESS_CONTEXT_REVEAL_APPROVED = 4;  // sudowork UI showing pw to human
  ACCESS_CONTEXT_AGENT_DIRECT = 5;     // agent tool got plaintext directly
}

message AuditContext {
  AccessContext access_context = 1;
  // Free-form caller tag, e.g. "sudowork", "password-agent".  Server
  // truncates to 256 bytes before persisting (DoS bound on the audit
  // table; protobuf strings are otherwise unbounded).
  optional string client_id = 2;
  // Free-form session id; sudowork passes its agent session here when
  // relevant.  Same 256-byte truncation as `client_id`.
  optional string agent_session = 3;
}

// --- Request / Response messages ---

message PutEntryRequest {
  VaultEntry entry = 1;
  AuditContext audit = 2;
}

message PutEntryResponse {
  string id = 1;
  string title = 2;
  int32 version = 3;
  google.protobuf.Timestamp created_at = 4;
}

message GetEntryRequest {
  string title = 1;
  // Specific historical version from ListVersions.  Unset = latest.
  optional int32 version = 2;
  AuditContext audit = 3;
  // `totp_secret` is always redacted in the response; clients that need
  // a code call `GenerateTotp` instead so the seed never crosses the
  // server boundary.
}

message GetEntryResponse {
  VaultEntry entry = 1;
  int32 version = 2;
}

message ListEntriesRequest {
  // Optional substring filter (case-insensitive over title/username/url/tags).
  // Currently REST does this client-side; server-side filter is fine.
  string query = 1;
  // 0 = no limit. REST defaults vary per consumer; gRPC explicit.
  int32 limit = 2;
  AuditContext audit = 3;
}

message ListEntriesResponse {
  repeated VaultEntry entries = 1;
  int32 total_in_vault = 2;  // before query filter
  int32 matched = 3;          // after query filter
}

message DeleteEntryRequest {
  string title = 1;
  AuditContext audit = 2;
}

message DeleteEntryResponse {
  string title = 1;
  bool deleted = 2;
}

message RestoreEntryRequest {
  string title = 1;
  AuditContext audit = 2;
}

message RestoreEntryResponse {
  string title = 1;
  bool restored = 2;
  int32 current_version = 3;
}

message Version {
  int32 version = 1;
  google.protobuf.Timestamp created_at = 2;
  // True if this version was the active one when the entry was soft-deleted.
  bool tombstoned = 3;
}

message ListVersionsRequest {
  string title = 1;
  AuditContext audit = 2;
}

message ListVersionsResponse {
  string title = 1;
  int32 count = 2;
  repeated Version versions = 3;
}

message GenerateTotpRequest {
  string title = 1;
  AuditContext audit = 2;
}

message GenerateTotpResponse {
  // 6-digit code. Always exactly 6 chars (RFC 6238 default).
  string code = 1;
  // Seconds until the current 30s window closes; clients that see < 3
  // typically wait and re-fetch to avoid race with input.
  int32 expires_in_seconds = 2;
  // Fixed 30 per RFC 6238; returned explicitly so clients don't bake it in.
  int32 period_seconds = 3;
}
