// AiKeysService -- BYOK provider key registration and inference-side
// fetch.
//
// Customers register their own provider key (e.g. an OpenAI API key);
// the inference handler (and, in milestone C, the dataplane) fetches it
// over the internal subdomain and uses the customer's key when
// forwarding to the provider.
//
// Plan reference: B2 in check-ia-and-plan-optimized-cook.md.
syntax = "proto3";

package tars.aikeys.v1;

import "google/api/annotations.proto";
import "google/api/field_behavior.proto";
import "google/protobuf/timestamp.proto";
import "tars/auth/v1/auth_options.proto";
import "tars/cli/v1/cli.proto";
import "tars/docs/v1/docs.proto";

option go_package = "github.com/tetrateio/fraser/api/tars/aikeys/v1;aikeysv1";

// Provider enumerates the upstream LLM providers TARS proxies. Adding a
// provider is a versioned change.
enum Provider {
  PROVIDER_UNSPECIFIED = 0;
  PROVIDER_OPENAI = 1;
  PROVIDER_ANTHROPIC = 2;
  PROVIDER_AZURE_OPENAI = 3;
  PROVIDER_GOOGLE_VERTEX = 4;
  PROVIDER_AWS_BEDROCK = 5;
}

// AiKey is the BYOK record. The plaintext is never returned after
// registration; the inference handler fetches the secret-svc handle
// and decrypts on demand.
//
// Storage / ownership model (table: ai_keys): user_id holds the user who
// created the key; key_address holds the "customer::project" scope.
//
// Two scopes share the same (customer, project) address:
//   - PROJECT-level keys have scope_user_id = NULL. Every project member
//     sees them and the project owner manages them. These are the
//     project's shared BYOK keys.
//   - USER-level keys have scope_user_id = <caller>. A member sets their
//     OWN key; it is private to that user and OVERRIDES the project key
//     for that user's inference traffic for the same provider.
//
// At inference time the resolver prefers the caller's user-scoped key for
// the provider and falls back to the project-level key. The legacy
// per-user "/byok" dashboard view instead filters on user_id.
message AiKey {
  // byok_<ulid>. Server-assigned.
  string id = 1 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Customer that owns the project this key is scoped to.
  string customer_id = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Project this key is registered under; List/Get/Revoke filter on it.
  string project_id = 3 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Upstream LLM provider this key authenticates against.
  Provider provider = 4 [(google.api.field_behavior) = REQUIRED];

  // Operator-supplied label, e.g. "openai-prod".
  string name = 5 [(google.api.field_behavior) = REQUIRED];

  // Last 4 chars of the registered plaintext, for human-friendly
  // display.
  string display_suffix = 6 [(google.api.field_behavior) = OUTPUT_ONLY];

  // user_scope is true when this is a per-user key (private to, and
  // managed by, scope_user_id) that overrides the project key for that
  // user. False for shared project-level keys.
  bool user_scope = 7 [(google.api.field_behavior) = OUTPUT_ONLY];

  // scope_user_id is the owning user for a user-scoped key; empty for a
  // project-level key.
  string scope_user_id = 8 [(google.api.field_behavior) = OUTPUT_ONLY];

  // When the key was registered.
  google.protobuf.Timestamp created_at = 50 [(google.api.field_behavior) = OUTPUT_ONLY];
  // When the key's metadata was last modified.
  google.protobuf.Timestamp updated_at = 51 [(google.api.field_behavior) = OUTPUT_ONLY];
  // When the key was revoked; unset while the key is active.
  google.protobuf.Timestamp revoked_at = 52 [(google.api.field_behavior) = OUTPUT_ONLY];

  // Reference to the secrets-svc record holding the wrapped plaintext.
  string secret_ref = 60 [(google.api.field_behavior) = OUTPUT_ONLY];
}

// AiKeysService exposes the customer-facing CRUD plus the internal Fetch.
service AiKeysService {
  // Register encrypts and stores a provider API key under a project.
  // After this call, inference requests routed to the matching provider
  // for this project will use this key.
  //
  // Scope: account-tier so any project member can register their OWN
  // user-scoped key (user_scope=true). Registering a PROJECT-level key
  // (user_scope=false) additionally requires the caller to be the
  // project's owner -- enforced in-handler against project_members.
  rpc Register(RegisterRequest) returns (RegisterResponse) {
    option (tars.docs.v1.title) = "Register an AI key";
    option (tars.docs.v1.group) = "AI Keys";
    option (google.api.http) = {
      post: "/v1/customers/{customer_id}/projects/{project_id}/aikeys"
      body: "*"
    };
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "byok",
        "add"
      ]
    };
  }

  // Get returns the metadata record for a single BYOK key. Never returns
  // the plaintext.
  //
  // Scope: account-tier. Any project member may read keys in the project
  // (project-level and the caller's own user-scoped keys); membership is
  // enforced in-handler.
  rpc Get(GetRequest) returns (GetResponse) {
    option (tars.docs.v1.title) = "Get an AI key";
    option (tars.docs.v1.group) = "AI Keys";
    option (google.api.http) = {get: "/v1/customers/{customer_id}/projects/{project_id}/aikeys/{id}"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "byok",
        "get"
      ]
      primary_flag: "id"
    };
  }

  // List lists BYOK keys registered under a project. Returns metadata
  // only -- id, provider, name, display_suffix, creation timestamp, and
  // revocation timestamp if revoked. The result includes the project's
  // shared keys plus the caller's own user-scoped keys (other users'
  // user-scoped keys are not returned).
  //
  // Scope: account-tier; membership is enforced in-handler.
  rpc List(ListRequest) returns (ListResponse) {
    option (tars.docs.v1.title) = "List AI keys";
    option (tars.docs.v1.group) = "AI Keys";
    option (google.api.http) = {get: "/v1/customers/{customer_id}/projects/{project_id}/aikeys"};
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "byok",
        "list"
      ]
    };
  }

  // Revoke marks the key revoked; subsequent Fetches return
  // permission_denied. Inflight cached keys on dataplanes invalidate
  // within sub-second via the revocation channel (milestone C).
  //
  // Scope: account-tier so a member can revoke their OWN user-scoped key.
  // Revoking a PROJECT-level key additionally requires project ownership
  // (enforced in-handler); a member may never revoke another user's
  // user-scoped key.
  rpc Revoke(RevokeRequest) returns (RevokeResponse) {
    option (tars.docs.v1.title) = "Revoke an AI key";
    option (tars.docs.v1.group) = "AI Keys";
    option (google.api.http) = {delete: "/v1/customers/{customer_id}/projects/{project_id}/aikeys/{id}"};
    option (tars.auth.v1.auth) = {
      auth_types: [
        AUTH_TYPE_USER_TOKEN,
        AUTH_TYPE_API_KEY
      ]
    };
    option (tars.cli.v1.command) = {
      path: [
        "byok",
        "remove"
      ]
      primary_flag: "id"
    };
  }

  // Fetch is the internal-only RPC the inference handler / dataplane
  // calls on the hot path. Returns the plaintext provider key.
  rpc Fetch(FetchRequest) returns (FetchResponse) {
    option idempotency_level = NO_SIDE_EFFECTS;
    option (tars.auth.v1.auth) = {
      auth_types: [AUTH_TYPE_SERVICE_ACCOUNT]
      scopes: ["internal"]
    };
    option (tars.cli.v1.command) = {hidden: true};
  }
}

message RegisterRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
  // Provider this key authenticates against.
  Provider provider = 3 [(google.api.field_behavior) = REQUIRED];
  // Operator-supplied label for the key, e.g. "openai-prod".
  string name = 4 [(google.api.field_behavior) = REQUIRED];

  // Plaintext provider key, e.g. sk-.... Sent over TLS only; the server
  // immediately wraps with secrets-svc and discards the plaintext.
  string secret = 5 [
    (google.api.field_behavior) = REQUIRED,
    (tars.cli.v1.field) = {flag: "key"}
  ];

  // user_scope registers a per-user key private to the caller that
  // overrides the project key for the caller's inference traffic. When
  // false (default) the key is project-level (shared) and requires the
  // caller to be the project owner.
  bool user_scope = 6 [(tars.cli.v1.field) = {flag: "user-scope"}];
}

message RegisterResponse {
  // The newly registered key's metadata record (no plaintext).
  AiKey key = 1;
}

message GetRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
  // byok_<ulid> identifying the key to fetch.
  string id = 3 [(google.api.field_behavior) = REQUIRED];
}

message GetResponse {
  // The requested key's metadata record (no plaintext).
  AiKey key = 1;
}

message ListRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
}

message ListResponse {
  // All keys registered under the project (metadata only, no plaintext).
  repeated AiKey keys = 1;
}

message RevokeRequest {
  // Defaults to the caller's session customer when empty.
  string customer_id = 1 [(tars.cli.v1.field) = {flag: "customer"}];
  // Defaults to the caller's session project when empty.
  string project_id = 2 [(tars.cli.v1.field) = {flag: "project"}];
  // byok_<ulid> identifying the key to revoke.
  string id = 3 [(google.api.field_behavior) = REQUIRED];
}

message RevokeResponse {}

message FetchRequest {
  // byok_<ulid> of the active key to decrypt and return.
  string id = 1 [(google.api.field_behavior) = REQUIRED];
}

message FetchResponse {
  // Provider the returned key authenticates against.
  Provider provider = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Decrypted plaintext provider key for the inference hot path.
  string secret = 2 [(google.api.field_behavior) = OUTPUT_ONLY];
  // Last 4 chars of the plaintext, for human-friendly display.
  string display_suffix = 3 [(google.api.field_behavior) = OUTPUT_ONLY];
}
