openapi: 3.0.3
info:
  title: OctoStore API
  description: >-
    Simple leader election over HTTP. Elect one remote leader without an account,
    API key, or SDK. Self-host the same API plus durable task locks, sessions,
    webhooks, and fencing terms in a single Rust binary backed by SQLite.
  version: 0.13.2
  contact:
    name: OctoStore
    url: https://octostore.io
  license:
    name: MIT
servers:
- url: https://api.octostore.io
  description: Production server
- url: http://localhost:3000
  description: Development server
security:
- bearerAuth: []
paths:
  /elections:
    post:
      tags:
      - Leader Elections
      summary: Create an account-free election room
      description: Generates a collision-resistant room ID. No login, API key, or request body is required.
      security: []
      responses:
        '201':
          description: Election room created
          content:
            application/json:
              schema:
                type: object
                required: [election_id, campaign_path, status_path]
                properties:
                  election_id:
                    type: string
                    example: g15BTc54MStBoX-momDJm6uBhJQKKtqQ
                  campaign_path:
                    type: string
                  status_path:
                    type: string
        '403':
          $ref: '#/components/responses/PublicElectionsDisabled'
        '429':
          $ref: '#/components/responses/PublicElectionRateLimited'
  /elections/{election_id}:
    get:
      tags:
      - Leader Elections
      summary: Inspect the current leader
      description: Publicly returns the current leader, term, lease expiry, and recommended retry delay.
      security: []
      parameters:
      - $ref: '#/components/parameters/ElectionId'
      responses:
        '200':
          description: Current election state
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ElectionStatus'
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/PublicElectionsDisabled'
  /elections/{election_id}/campaign:
    post:
      tags:
      - Leader Elections
      summary: Campaign for leadership
      description: The first live candidate becomes leader. Followers receive the current leader and a retry delay. No account or API key is required.
      security: []
      parameters:
      - $ref: '#/components/parameters/ElectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [candidate_id]
              properties:
                candidate_id:
                  type: string
                  minLength: 1
                  maxLength: 64
                  pattern: '^[A-Za-z0-9._:@-]+$'
                  example: worker-west-2
                ttl_seconds:
                  type: integer
                  minimum: 5
                  maximum: 300
                  default: 30
                metadata:
                  type: string
                  maxLength: 1024
                  example: model=gpt-5 task=release-1842
      responses:
        '200':
          description: Candidate is the leader or a follower
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/ElectionLeaderResult'
                - $ref: '#/components/schemas/ElectionFollowerResult'
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/PublicElectionsDisabled'
        '409':
          description: Public election capacity reached
        '429':
          $ref: '#/components/responses/PublicElectionRateLimited'
  /elections/{election_id}/renew:
    post:
      tags:
      - Leader Elections
      summary: Renew leadership
      description: Extends the current term using the opaque leader capability returned by a winning campaign.
      security: []
      parameters:
      - $ref: '#/components/parameters/ElectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LeaderAction'
      responses:
        '200':
          description: Leadership renewed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ElectionLeaderResult'
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/PublicElectionsDisabled'
        '404':
          description: Election has no current leader
  /elections/{election_id}/resign:
    post:
      tags:
      - Leader Elections
      summary: Resign leadership
      description: Releases the current term immediately using its opaque leader capability.
      security: []
      parameters:
      - $ref: '#/components/parameters/ElectionId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [leader_token]
              properties:
                leader_token:
                  type: string
                  description: Opaque capability returned to the current leader
      responses:
        '200':
          description: Leadership resigned and the room is vacant
          content:
            application/json:
              schema:
                type: object
                required: [election_id, status, previous_term]
                properties:
                  election_id:
                    type: string
                  status:
                    type: string
                    enum: [vacant]
                  previous_term:
                    type: integer
                    format: int64
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/PublicElectionsDisabled'
        '404':
          description: Election has no current leader
  /auth/github:
    get:
      tags:
      - Authentication
      summary: Initiate GitHub OAuth flow
      description: Redirects to GitHub OAuth authorization page
      security: []
      responses:
        '302':
          description: Redirect to GitHub OAuth
          headers:
            Location:
              schema:
                type: string
                example: https://github.com/login/oauth/authorize?client_id=...
  /auth/github/callback:
    get:
      tags:
      - Authentication
      summary: GitHub OAuth callback
      description: Internal callback endpoint for GitHub OAuth flow
      security: []
      parameters:
      - name: code
        in: query
        required: true
        schema:
          type: string
        description: OAuth authorization code from GitHub
      - name: state
        in: query
        required: false
        schema:
          type: string
        description: OAuth state parameter
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: Bearer token for API access
                  user:
                    type: object
                    properties:
                      login:
                        type: string
                        description: GitHub username
                      avatar_url:
                        type: string
                        description: GitHub avatar URL
                required:
                - token
                - user
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/token/rotate:
    post:
      tags:
      - Authentication
      summary: Rotate bearer token
      description: Generate a new bearer token, invalidating the current one
      responses:
        '200':
          description: Token rotated successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    description: New bearer token
                required:
                - token
        '401':
          $ref: '#/components/responses/Unauthorized'
  /locks/{name}/acquire:
    post:
      tags:
      - Locks
      summary: Acquire a lock
      description: 'Attempt to acquire a distributed lock with the given name.

        Returns the lock status and lease information if successful.

        '
      parameters:
      - $ref: '#/components/parameters/LockName'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ttl_seconds:
                  type: integer
                  minimum: 1
                  maximum: 3600
                  description: Lock time-to-live in seconds (max 1 hour)
                  example: 60
              required:
              - ttl_seconds
      responses:
        '200':
          description: Lock acquired or already held by caller
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LockResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Lock currently held by another user
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LockConflict'
        '422':
          $ref: '#/components/responses/ValidationError'
  /locks/{name}/release:
    post:
      tags:
      - Locks
      summary: Release a lock
      description: Release a previously acquired lock using its lease ID
      parameters:
      - $ref: '#/components/parameters/LockName'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                lease_id:
                  type: string
                  format: uuid
                  description: Lease ID returned when lock was acquired
                  example: 550e8400-e29b-41d4-a716-446655440000
              required:
              - lease_id
      responses:
        '200':
          description: Lock released successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - released
                  message:
                    type: string
                    example: Lock released
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/LockNotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /locks/{name}/renew:
    post:
      tags:
      - Locks
      summary: Renew a lock's TTL
      description: Extend the time-to-live of an existing lock
      parameters:
      - $ref: '#/components/parameters/LockName'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                lease_id:
                  type: string
                  format: uuid
                  description: Lease ID of the lock to renew
                  example: 550e8400-e29b-41d4-a716-446655440000
                ttl_seconds:
                  type: integer
                  minimum: 1
                  maximum: 3600
                  description: New TTL in seconds (max 1 hour)
                  example: 300
              required:
              - lease_id
              - ttl_seconds
      responses:
        '200':
          description: Lock renewed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LockResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/LockNotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /locks/{name}:
    get:
      tags:
      - Locks
      summary: Get lock status
      description: Check the current status of a specific lock
      parameters:
      - $ref: '#/components/parameters/LockName'
      responses:
        '200':
          description: Lock status retrieved
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/LockResponse'
                - $ref: '#/components/schemas/LockNotHeld'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /locks:
    get:
      tags:
      - Locks
      summary: List locks by prefix
      description: |
        List all currently-held locks, optionally filtered by a hierarchical
        name prefix. Lock names use `/` as the path separator
        (e.g. `service/component/resource`).
      parameters:
      - name: prefix
        in: query
        required: false
        schema:
          type: string
        description: 'Optional prefix to filter locks (e.g. "service/" or "db/primary")'
        example: service/
      responses:
        '200':
          description: List of matching locks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListLocksResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /sessions:
    post:
      tags:
      - Sessions
      summary: Create a new session
      description: 'Create a session with a keepalive TTL. Locks acquired with this
        session_id will be automatically released when the session expires.

        '
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ttl_seconds:
                  type: integer
                  minimum: 10
                  maximum: 300
                  description: Session TTL in seconds (default 60, min 10, max 300)
                  example: 60
      responses:
        '201':
          description: Session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateSessionResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /sessions/{id}/keepalive:
    post:
      tags:
      - Sessions
      summary: Extend session expiry
      description: Send a keepalive to extend the session TTL by its original duration
      parameters:
      - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Session extended
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/KeepAliveResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '410':
          description: Session expired
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /sessions/{id}:
    get:
      tags:
      - Sessions
      summary: Get session status
      description: Returns session details including the number of locks held
      parameters:
      - $ref: '#/components/parameters/SessionId'
      responses:
        '200':
          description: Session status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionStatusResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    delete:
      tags:
      - Sessions
      summary: Terminate a session
      description: Explicitly close a session and release all associated locks
      parameters:
      - $ref: '#/components/parameters/SessionId'
      responses:
        '204':
          description: Session terminated
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /webhooks:
    post:
      tags:
      - Webhooks
      summary: Create a webhook
      description: |
        Register a webhook to receive POST callbacks on lock events.
        URL must use HTTPS. Maximum 10 webhooks per user.
        If a secret is provided, payloads are signed with HMAC-SHA256
        and the signature is sent in the X-OctoStore-Signature header.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
        '400':
          description: Invalid input (non-HTTPS URL or limit exceeded)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
    get:
      tags:
      - Webhooks
      summary: List your webhooks
      description: Returns all webhooks registered by the authenticated user.
        Secrets are redacted in the response.
      responses:
        '200':
          description: List of webhooks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/WebhookResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /webhooks/{id}:
    delete:
      tags:
      - Webhooks
      summary: Delete a webhook
      description: Remove a previously registered webhook
      parameters:
      - name: id
        in: path
        required: true
        schema:
          type: string
          format: uuid
        description: Webhook UUID
        example: 550e8400-e29b-41d4-a716-446655440000
      responses:
        '204':
          description: Webhook deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Webhook not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /health:
    get:
      tags:
      - System
      summary: Health check
      description: Simple health check endpoint
      security: []
      responses:
        '200':
          description: Service is healthy
          content:
            text/plain:
              schema:
                type: string
                example: OK
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Bearer token from optional GitHub OAuth or a self-hosted static token
  parameters:
    ElectionId:
      name: election_id
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-zA-Z0-9_-]{8,64}$
      description: Opaque generated room ID or a shared custom ID with at least 8 characters
      example: g15BTc54MStBoX-momDJm6uBhJQKKtqQ
    SessionId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Session UUID
      example: 550e8400-e29b-41d4-a716-446655440000
    LockName:
      name: name
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-zA-Z0-9._-]+(/[a-zA-Z0-9._-]+)*$
        minLength: 1
        maxLength: 256
      description: 'Hierarchical lock name using `/` as path separator. Components
        may contain alphanumeric characters, hyphens, underscores, and dots (max 64
        chars each).'
      example: service/component/my-lock
  schemas:
    ElectionLeader:
      type: object
      required: [candidate_id, term, expires_at]
      properties:
        candidate_id:
          type: string
          example: agent-west-2
        metadata:
          type: string
          nullable: true
        term:
          type: integer
          format: int64
          description: Monotonic fencing term. Downstream systems can reject work from older terms.
        expires_at:
          type: string
          format: date-time
    ElectionStatus:
      type: object
      required: [election_id, status, retry_after_ms]
      properties:
        election_id:
          type: string
        status:
          type: string
          enum: [leader, vacant]
        leader:
          allOf:
          - $ref: '#/components/schemas/ElectionLeader'
          nullable: true
        retry_after_ms:
          type: integer
          format: int64
    ElectionLeaderResult:
      type: object
      required: [status, election_id, leader, leader_token, renew_after_ms]
      properties:
        status:
          type: string
          enum: [leader]
        election_id:
          type: string
        leader:
          $ref: '#/components/schemas/ElectionLeader'
        leader_token:
          type: string
          description: Secret bearer capability required to renew or resign this term
        renew_after_ms:
          type: integer
          format: int64
    ElectionFollowerResult:
      type: object
      required: [status, election_id, leader, retry_after_ms]
      properties:
        status:
          type: string
          enum: [follower]
        election_id:
          type: string
        leader:
          $ref: '#/components/schemas/ElectionLeader'
        retry_after_ms:
          type: integer
          format: int64
    LeaderAction:
      type: object
      required: [leader_token]
      properties:
        leader_token:
          type: string
          description: Secret bearer capability returned to the current leader
        ttl_seconds:
          type: integer
          minimum: 5
          maximum: 300
          default: 30
    LockResponse:
      type: object
      properties:
        status:
          type: string
          enum:
          - acquired
          - held
          description: Lock status
        name:
          type: string
          description: Lock name
        lease_id:
          type: string
          format: uuid
          description: Unique lease identifier
        fencing_token:
          type: integer
          description: Monotonically increasing fencing token for safety
        expires_at:
          type: string
          format: date-time
          description: Lock expiration time in RFC3339 format
        holder:
          type: string
          description: GitHub username of the lock holder
      required:
      - status
      - name
      - lease_id
      - fencing_token
      - expires_at
      - holder
      example:
        status: acquired
        name: my-resource
        lease_id: 550e8400-e29b-41d4-a716-446655440000
        fencing_token: 42
        expires_at: '2024-01-01T12:00:00Z'
        holder: username
    LockConflict:
      type: object
      properties:
        error:
          type: string
          example: Lock already held
        lock:
          type: object
          properties:
            name:
              type: string
            holder:
              type: string
              description: Current holder's GitHub username
            expires_at:
              type: string
              format: date-time
      required:
      - error
      - lock
    LockNotHeld:
      type: object
      properties:
        status:
          type: string
          enum:
          - available
        name:
          type: string
        message:
          type: string
          example: Lock not currently held
      required:
      - status
      - name
    LockInfo:
      type: object
      properties:
        name:
          type: string
          description: Lock name
        lease_id:
          type: string
          format: uuid
          description: Lease identifier
        fencing_token:
          type: integer
          description: Current fencing token
        expires_at:
          type: string
          format: date-time
          description: Lock expiration time
      required:
      - name
      - lease_id
      - fencing_token
      - expires_at
    ListLocksResponse:
      type: object
      properties:
        locks:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
                description: Lock name
              status:
                type: string
                enum:
                - held
                description: Lock status (always "held" for active locks)
              holder_id:
                type: string
                format: uuid
                description: UUID of the lock holder
              fencing_token:
                type: integer
                description: Monotonically increasing fencing token
              expires_at:
                type: string
                format: date-time
                description: Lock expiration time
              metadata:
                type: string
                nullable: true
                description: User-defined metadata
            required:
            - name
            - status
            - fencing_token
        total:
          type: integer
          description: Total number of locks returned
          example: 5
        prefix:
          type: string
          nullable: true
          description: The prefix filter that was applied (null if none)
          example: service/
      required:
      - locks
      - total
    CreateSessionResponse:
      type: object
      properties:
        session_id:
          type: string
          format: uuid
          description: Unique session identifier
        expires_at:
          type: string
          format: date-time
          description: Session expiration time
        keepalive_interval_secs:
          type: integer
          description: Recommended keepalive interval (ttl / 2)
      required:
      - session_id
      - expires_at
      - keepalive_interval_secs
    KeepAliveResponse:
      type: object
      properties:
        session_id:
          type: string
          format: uuid
        expires_at:
          type: string
          format: date-time
      required:
      - session_id
      - expires_at
    SessionStatusResponse:
      type: object
      properties:
        session_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        expires_at:
          type: string
          format: date-time
        lock_count:
          type: integer
          description: Number of locks held by this session
        active:
          type: boolean
          description: Whether the session is still active
      required:
      - session_id
      - user_id
      - expires_at
      - lock_count
      - active
    CreateWebhookRequest:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: HTTPS URL to POST event payloads to
          example: https://example.com/hooks/octostore
        secret:
          type: string
          description: Optional HMAC-SHA256 signing secret
          example: my-secret-key
        events:
          type: array
          items:
            type: string
            enum:
            - acquired
            - released
            - expired
            - renewed
            - '*'
          description: 'Event types to subscribe to (default: ["*"])'
          example:
          - acquired
          - released
        lock_pattern:
          type: string
          description: 'Glob pattern to filter locks (e.g. "service/*"). Omit for
            all locks.'
          example: service/*
      required:
      - url
    WebhookResponse:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Webhook identifier
        user_id:
          type: string
          format: uuid
          description: Owner user ID
        url:
          type: string
          format: uri
          description: Callback URL
        secret:
          type: string
          nullable: true
          description: Redacted secret (shown as "****" if set)
        events:
          type: array
          items:
            type: string
          description: Subscribed event types
        lock_pattern:
          type: string
          nullable: true
          description: Lock name glob pattern
        created_at:
          type: string
          format: date-time
          description: Creation timestamp
        active:
          type: boolean
          description: Whether the webhook is active
      required:
      - id
      - user_id
      - url
      - events
      - created_at
      - active
    WebhookPayload:
      type: object
      description: Payload POSTed to webhook URLs on lock events
      properties:
        event:
          type: string
          enum:
          - acquired
          - released
          - expired
          - renewed
          description: Lock event type
        lock:
          type: string
          description: Lock name
        holder_id:
          type: string
          format: uuid
          nullable: true
          description: UUID of the lock holder (null for release/expiry)
        fencing_token:
          type: integer
          nullable: true
          description: Fencing token (null for release/expiry)
        timestamp:
          type: string
          format: date-time
          description: Event timestamp in RFC3339 format
      required:
      - event
      - lock
      - timestamp
    Error:
      type: object
      properties:
        error:
          type: string
          description: Error message
        details:
          type: string
          description: Additional error details (optional)
      required:
      - error
  responses:
    PublicElectionRateLimited:
      description: This client exceeded the configured public election admission rate
      headers:
        Retry-After:
          description: Seconds until the client's current admission window resets
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Rate limit exceeded
            details: Rate limit exceeded; retry in 42 seconds
    PublicElectionsDisabled:
      description: Public elections are disabled on this server
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Public elections are disabled on this server
    Unauthorized:
      description: Invalid or missing authentication token
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Invalid or missing authorization header
    LockNotFound:
      description: Lock not found or not owned by caller
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Lock not found or not owned by caller
    ValidationError:
      description: Request validation failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalid_ttl:
              summary: Invalid TTL
              value:
                error: TTL must be between 1 and 3600 seconds
            invalid_uuid:
              summary: Invalid lease ID
              value:
                error: Invalid lease_id format
tags:
- name: Leader Elections
  description: Account-free, capability-based leader election over HTTP
- name: Authentication
  description: GitHub OAuth and token management
- name: Sessions
  description: Session management with keepalive
- name: Locks
  description: Distributed lock operations
- name: Webhooks
  description: Webhook registration and management for lock event callbacks
- name: System
  description: System health and status
