{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://aiir.jacobideji.com/schemas/credential-event.schema.json",
  "title": "Credential Event Log Entry",
  "description": "JSON Schema for a single credential-event log entry. The credential-event log is the upstream contract specified in playbooks/07-secrets-and-tokens.md (Boundary 3: Telemetry) and consumed by playbooks/11-monitoring-detection.md Family 3 (capability-based signals). Every credential lifecycle change (issuance, refresh, revocation, scope adjustment) must emit one event matching this schema to the customer's SIEM. The schema's 9 required fields are PB07's canonical field set; the event_type enum splits PB07's 'scope_change' into expansion vs reduction for detection clarity.",
  "type": "object",
  "required": ["agent_id", "principal", "event_type", "prior_scopes", "new_scopes", "timestamp", "actor", "justification", "ticket_id"],
  "properties": {
    "agent_id": {
      "type": "string",
      "pattern": "^[a-z0-9-]+$",
      "description": "The agent identifier. Must match templates/ai-bom.yaml `agent.name` field for cross-record join. Lowercase alphanumeric with hyphens."
    },
    "principal": {
      "type": "string",
      "minLength": 1,
      "description": "The identity principal whose credential is changing (service account email, OAuth grant subject, delegated impersonation user, etc.). Must match templates/ai-bom.yaml `identity.principal` field for the agent."
    },
    "event_type": {
      "enum": ["issuance", "refresh", "revocation", "scope_expansion", "scope_reduction"],
      "description": "The credential lifecycle event class. issuance = initial credential creation. refresh = token refresh with no scope change. revocation = credential disabled and all scopes removed. scope_expansion = scopes added (PB11 Family 3 capability-based signal). scope_reduction = scopes removed (often the M5 Controlled Re-Enable validation per PB07 scope-shrink rule). Adopters following PB07's prose 'scope_change' category must map to either scope_expansion or scope_reduction; this schema requires the more specific value for detection clarity."
    },
    "prior_scopes": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Scopes held before the event. Empty array for issuance of a brand-new principal. Required even when empty so consumers can compute scope diffs deterministically without null-handling logic."
    },
    "new_scopes": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Scopes held after the event. Empty array for revocation events (enforced by schema). Required even when empty."
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "RFC 3339 timestamp when the credential change took effect, not when the event was logged. Identity-provider timestamps are preferred over application-side timestamps because they are the audit-defensible source for credential lifecycle events."
    },
    "actor": {
      "type": "string",
      "minLength": 1,
      "description": "Who or what triggered the event. Human admin email, service account name, automation pipeline identifier, or 'system' for provider-initiated events. Anonymous attribution ('admin', 'system') is acceptable only when no more-specific attribution exists; the upstream IdP almost always has a specific actor."
    },
    "justification": {
      "type": "string",
      "minLength": 1,
      "description": "Free-text reason for the credential change. Required and non-empty. Per playbooks/07-secrets-and-tokens.md scope-shrink rule, scope changes without documented business justification are a Common Pitfall. The justification enters the case file when this event becomes relevant to a later incident investigation."
    },
    "ticket_id": {
      "type": "string",
      "minLength": 1,
      "description": "Reference to the change-management ticket authorizing the credential change. Required even for automated rotations. Format is adopter-specific (JIRA key, ServiceNow ticket number, GitHub issue URL, etc.). Per the framework's audit-trail discipline, every credential change has a documented authorization path."
    }
  },
  "allOf": [
    {
      "$comment": "Semantic constraint: revocation events must end with empty new_scopes. A partial revocation (some scopes removed but credential still active) should be logged as scope_reduction, not revocation.",
      "if": {
        "properties": { "event_type": { "const": "revocation" } },
        "required": ["event_type"]
      },
      "then": {
        "properties": {
          "new_scopes": { "maxItems": 0 }
        }
      }
    }
  ]
}
