aviso-server 0.6.0

Notification service for data-driven workflows with live and replay APIs.
# Aviso Server example configuration (local-test friendly).
# Adjust URLs, secrets, and schema fields for your environment.

application:
  # HTTP bind address/port.
  host: "127.0.0.1"
  port: 8000
  # Used in generated metadata links.
  base_url: "http://localhost"
  # Landing-page assets.
  static_files_path: "./src/static"

watch_endpoint:
  # SSE heartbeat interval (seconds).
  sse_heartbeat_interval_sec: 30
  # Max watch connection lifetime (seconds).
  connection_max_duration_sec: 3600
  # Replay tuning.
  replay_batch_size: 100
  max_historical_notifications: 10000
  replay_batch_delay_ms: 100
  # Parallel event conversion workers.
  concurrent_notification_processing: 15

notification_backend:
  # `in_memory` or `jetstream`.
  kind: in_memory
  in_memory:
    # Retained messages per topic.
    max_history_per_topic: 10000
    # Tracked topics.
    max_topics: 100

# Authentication and authorization
# - disabled: API is public only when schemas do not declare stream auth rules
# - enabled:
#   - admin endpoints always require auth + one of `admin_roles`
#   - notify/watch/replay enforce auth only when stream has `auth.required: true`
#   - if auth-o-tron is unreachable, authenticated requests fail with 503
auth:
  # Enable authentication middleware.
  enabled: true

  # Authentication mode:
  # - direct: Aviso authenticates credentials via auth-o-tron (JWT/Basic).
  # - trusted_proxy: Aviso validates a forwarded Bearer JWT using jwt_secret.
  mode: direct

  # auth-o-tron base URL (required when enabled=true and mode=direct).
  auth_o_tron_url: "http://localhost:8080"

  # JWT secret (required when enabled=true).
  # Keep empty when auth is disabled.
  # Must match auth-o-tron signing configuration.
  # For scripts/example_auth_config.yaml, this should be "your-shared-secret".
  jwt_secret: "your-shared-secret"

  # Realm-scoped roles allowed on /api/v1/admin/* (must be non-empty when enabled=true).
  # Maps realm names (from JWT `realm` claim) to authorized role lists.
  admin_roles:
    localrealm: ["admin"]

  # auth-o-tron request timeout (milliseconds).
  timeout_ms: 5000

  # trusted_proxy mode validates forwarded Bearer JWT using jwt_secret.
  # No extra trusted_proxy block is needed.

# ECPDS destination authorization plugin (optional).
#
# Only honored when the binary is built with `--features ecpds`. Streams
# can opt in to this check by adding `plugins: ["ecpds"]` under their
# `auth` block (see the dissemination schema below).
#
# IMPORTANT: a build WITHOUT `--features ecpds` will refuse to start if
# any schema references the ecpds plugin, so the `plugins: ["ecpds"]`
# line and this whole section are commented out by default. Uncomment
# both together when running a build with the feature enabled.
#
# See docs/src/configuration-reference.md#ecpds for every field, and
# docs/src/authentication.md#ecpds-destination-authorization for the
# operator-facing setup guide.
#
# ecpds:
#   # Service-account credentials for the ECPDS HTTP API (Basic auth).
#   username: "ecpds-service-account"
#   password: "service-password"
#   # ECPDS server base URLs. Use https:// for any reachable host: the
#   # plugin authenticates with HTTP Basic Auth, so plain http to a real
#   # host would put the service-account password and per-user
#   # destination lookups on the wire without TLS. Plain http:// is
#   # accepted only for loopback ({127.0.0.1, [::1], localhost}) so the
#   # mockito-based test fixtures can keep working; a typo from
#   # https:// to http:// on a non-loopback host fails at startup.
#   # No query string, no fragment.
#   # All servers are queried per request and merged per partial_outage_policy.
#   servers:
#     - "https://ecpds-primary.example.int"
#     - "https://ecpds-secondary.example.int"
#   # Identifier field whose value is treated as the destination to
#   # authorise. Must be present in the schema's `identifier` with
#   # `required: true` so the value is guaranteed before the plugin
#   # runs. The plugin reads it from the request's canonicalized
#   # identifier params, NOT from topic routing, so the field does
#   # NOT need to appear in `topic.key_order`.
#   match_key: "destination"
#   # JSON field of each ECPDS destination record matched against the
#   # request's match_key value. Default: "name".
#   target_field: "name"
#   # Cache TTL per user (seconds). Default: 300.
#   cache_ttl_seconds: 300
#   # Maximum cache entries before moka TinyLFU eviction. Default: 10000.
#   max_entries: 10000
#   # Per-request HTTP timeouts (seconds). Defaults: 30 / 5.
#   request_timeout_seconds: 30
#   connect_timeout_seconds: 5
#   # How tolerant the merge is when one configured server fails.
#   # The destination list itself is always the union of per-server
#   # responses; this option only governs failure handling.
#   # - strict (default): every server must respond successfully or the
#   #   whole call returns 503. One ECPDS server going down takes the
#   #   plugin to 503 until it returns.
#   # - any_success: take the union of whichever servers responded
#   #   successfully within the per-request timeout. Only fails when no
#   #   server responded usefully. Useful for federated deployments
#   #   where each server covers a different destination namespace and
#   #   you want to keep serving during a partial outage.
#   partial_outage_policy: strict

metrics:
  # Enable Prometheus metrics on a separate internal port.
  enabled: false
  # Bind address for the metrics server. Defaults to 127.0.0.1 (loopback).
  # host: "127.0.0.1"
  # Required when enabled=true; must differ from application.port.
  # port: 9090

logging:
  # OTel-aligned JSON logs.
  level: info
  format: json

# Schema definitions
# Each stream defines topic shape, identifier validation, payload requirement,
# and optional auth policy. These schemas are used by scripts/smoke_test.py.
notification_schema:
  # Public stream (no auth block => anonymous access even when auth.enabled=true).
  test_polygon:
    payload:
      required: true
    topic:
      base: "polygon"
      key_order: ["date", "time"]
    identifier:
      date:
        type: DateHandler
        canonical_format: "%Y%m%d"
        required: false
      time:
        type: TimeHandler
        required: false
      polygon:
        type: PolygonHandler
        required: true

  # Authenticated stream with read/write role separation.
  # - Any authenticated user in localrealm can read (wildcard).
  # - Only producer or admin roles can write.
  mars:
    payload:
      required: false
    topic:
      base: "mars"
      key_order: ["class", "expver", "domain", "date", "time", "stream", "step"]
    auth:
      required: true
      read_roles:
        localrealm: ["*"]
      write_roles:
        localrealm: ["producer"]
    identifier:
      class:
        type: StringHandler
        max_length: 2
        required: true
      date:
        type: DateHandler
        canonical_format: "%Y%m%d"
        required: false
      domain:
        type: EnumHandler
        values: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
        required: false
      expver:
        type: ExpverHandler
        default: "0001"
        required: false
      step:
        type: IntHandler
        range: [0, 100000]
        required: false
      stream:
        type: StringHandler
        required: false
      time:
        type: TimeHandler
        required: false

  # Authenticated stream with admin-only writes.
  # - Any authenticated user can read (no read_roles restriction).
  # - Only admins can write (no write_roles => defaults to admin_roles).
  dissemination:
    payload:
      required: true
    topic:
      base: "diss"
      key_order: ["destination", "target", "class", "expver", "domain", "date", "time", "stream", "step"]
    auth:
      required: true
      # Uncomment the next line ONLY when running a build compiled with
      # `--features ecpds` and after configuring the top-level `ecpds:`
      # section above. Without that, startup will fail loudly. The plugin
      # adds destination-level access control on watch/replay using the
      # `destination` identifier field below as the match_key.
      # plugins: ["ecpds"]
    identifier:
      class:
        type: StringHandler
        max_length: 2
        required: true
      date:
        type: DateHandler
        canonical_format: "%Y%m%d"
        required: false
      destination:
        type: StringHandler
        required: true
      target:
        type: StringHandler
        required: false
      domain:
        type: EnumHandler
        values: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
        required: false
      expver:
        type: ExpverHandler
        default: "0001"
        required: false
      step:
        type: IntHandler
        range: [0, 100000]
        required: false
      stream:
        type: StringHandler
        required: false
      time:
        type: TimeHandler
        required: false

# Smoke test auth verification (with scripts/example_auth_config.yaml):
# 1) test_polygon without Authorization => 200 (no auth block)
# 2) mars without Authorization => 401 (auth.required=true)
# 3) mars with reader-user => read 200, write 403
# 4) mars with producer-user => write 200
# 5) mars with admin-user => read 200, write 200
# 6) dissemination with reader-user => read 200, write 403 (admin-only writes)
# 7) dissemination with admin-user => read 200, write 200
#
# When the optional ECPDS plugin is enabled (top-level ecpds: section
# above is uncommented AND a stream has plugins: ["ecpds"]) the smoke
# script can additionally verify allow/deny against your real ECPDS.
# See docs/src/getting-started.md#optional-end-to-end-ecpds-plugin-smoke-test.