CHANGELOG.md

# Changelog

All notable changes to this project will be documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.1] — 2025-03-16

### Added

#### API

- `Aurinko.Auth` — OAuth authorization URL builder, code exchange, and token refresh
- `Aurinko.APIs.Email` — List, get, send, draft, update messages; delta sync; attachments; email tracking
- `Aurinko.APIs.Calendar` — List/get calendars and events; create/update/delete events; delta sync; free/busy
- `Aurinko.APIs.Contacts` — CRUD contacts; delta sync
- `Aurinko.APIs.Tasks` — Task list and task management (list, create, update, delete)
- `Aurinko.APIs.Webhooks` — Subscription management (list, create, delete)
- `Aurinko.APIs.Booking` — Booking profile listing and availability
- `Aurinko.Types` — Typed structs for Email, CalendarEvent, Calendar, Contact, Task, Pagination, SyncResult
- `Aurinko.Error` — Structured, tagged error type with HTTP status mapping
- `Aurinko.HTTP.Client` — Req-based HTTP client with retry, backoff, and connection pooling
- `Aurinko.Telemetry` — `:telemetry` events for all HTTP requests
- `Aurinko.Config` — NimbleOptions-validated configuration
- Full typespecs and `@moduledoc`/`@doc` coverage
- GitHub Actions CI with matrix testing (Elixir 1.16/1.17, OTP 26/27)
- Credo strict linting and Dialyzer integration

#### Middleware & Infrastructure

- **`Aurinko.Cache`** — ETS-backed TTL response cache for all `GET` requests
  - Configurable TTL (default 60 s), max size (default 5 000 entries), and cleanup interval
  - LRU eviction when the entry limit is reached
  - Per-token cache invalidation via `invalidate_token/1`
  - Hit/miss/eviction statistics via `stats/0`
  - SHA-256 cache key derivation from `{token, path, params}`
  - `get/1`, `put/3`, `delete/1`, `flush/0`, `build_key/3` public API

- **`Aurinko.RateLimiter`** — Token-bucket rate limiter with dual buckets
  - Per-token bucket (default 10 req/s) and global bucket (default 100 req/s)
  - Configurable burst allowance (default +5 over steady-state)
  - Returns `:ok` or `{:wait, ms}` — the HTTP client sleeps and continues automatically
  - `check_rate/1`, `reset_token/1`, `inspect_bucket/1` public API
  - ETS-backed buckets with automatic cleanup of stale entries after 5 min of inactivity

- **`Aurinko.CircuitBreaker`** — Per-endpoint circuit breaker (closed → open → half-open)
  - Configurable failure threshold (default 5) and recovery timeout (default 30 s)
  - Tracks `server_error`, `network_error`, and `timeout` failure types; ignores `not_found` etc.
  - Half-open probe on timeout expiry; re-opens on probe failure, closes on probe success
  - `call/2`, `status/1`, `reset/1` public API
  - ETS-backed state machine; named per normalised URL path (IDs replaced with `:id`)

#### HTTP Client (rewritten)

- **`Aurinko.HTTP.Client`** — Req 0.5-based HTTP client with full middleware pipeline
  - Pipeline order: Rate Limiting → Cache Lookup → Circuit Breaker → HTTP + Retry → Cache Write → Telemetry
  - Exponential backoff with jitter for `429` and `5xx` responses
  - `Retry-After` header parsing for `429` responses
  - Structured `%Aurinko.Error{}` on all failure paths (no raw exceptions leak)
  - Request packing via `req_info` map to keep internal function arities ≤ 8
  - `get/3`, `post/4`, `patch/4`, `put/4`, `delete/3` public API

#### Streaming Pagination

- **`Aurinko.Paginator`** — Lazy `Stream`-based pagination for all list endpoints
  - `stream/3` — streams records across all pages on demand, never loading all into memory
  - `sync_stream/4` — streams delta-sync records; captures `next_delta_token` via `:on_delta` callback
  - `collect_all/3` — synchronous convenience wrapper returning `{:ok, list}`
  - Configurable `:on_error` — `:halt` (default) or `:skip` per-page error handling

#### Sync Orchestrator

- **`Aurinko.Sync.Orchestrator`** — High-level delta-sync lifecycle manager
  - `sync_email/2` — full or incremental email sync; resolves or provisions delta tokens automatically
  - `sync_calendar/3` — calendar sync with configurable `time_min`/`time_max` window
  - `sync_contacts/2` — contacts sync (updated records only; no deleted stream)
  - Accepts `:get_tokens`, `:save_tokens`, `:on_updated`, `:on_deleted` callbacks
  - Automatic retry with backoff when Aurinko sync is not yet `:ready`
  - Records are delivered in batches of 200 via `Stream.chunk_every/2`

#### Webhook Support

- **`Aurinko.Webhook.Verifier`** — HMAC-SHA256 signature verification
  - `verify/3` — validates `sha256=<hex>` signature header; returns `:ok` or `{:error, :invalid_signature}`
  - `sign/2` — test helper for generating valid signatures
  - Constant-time comparison via `:crypto.hash/2` to prevent timing attacks (no `plug_crypto` dependency)

- **`Aurinko.Webhook.Handler`** — Behaviour + dispatcher for webhook event processing
  - `dispatch/4` — parses raw body, optionally verifies signature, routes `eventType` to handler module
  - `@callback handle_event/3` behaviour for implementing custom handlers

#### Observability

- **`Aurinko.Telemetry`** (expanded) — 7 telemetry events now emitted
  - `[:aurinko, :request, :start]` — before each HTTP request
  - `[:aurinko, :request, :stop]` — after each HTTP request (includes duration, cached flag)
  - `[:aurinko, :request, :retry]` — on each retry attempt (includes reason: `:rate_limited`, `:server_error`, `:timeout`)
  - `[:aurinko, :circuit_breaker, :opened]` — when a circuit opens (threshold exceeded or probe failure)
  - `[:aurinko, :circuit_breaker, :closed]` — when a circuit recovers
  - `[:aurinko, :circuit_breaker, :rejected]` — when a request is rejected by an open circuit
  - `[:aurinko, :sync, :complete]` — after a full sync run (updated count, deleted count, duration)
  - `attach_default_logger/0` and `detach_default_logger/0` for zero-config structured logging
  - `Telemetry.Metrics` definitions for Prometheus / StatsD reporters

- **`Aurinko.Logger.JSONFormatter`** — Structured JSON log formatter
  - One JSON object per log line: `time`, `level`, `msg`, `pid`, `module`, `function`, `line`, `request_id`
  - Compatible with Datadog, Loki, Google Cloud Logging, and other log aggregation pipelines
  - Plug-in via `config :logger, :console, format: {Aurinko.Logger.JSONFormatter, :format}`

#### OTP Application

- **`Aurinko.Application`** — Supervised OTP application with ordered start-up
  - Supervision order: Cache → RateLimiter → CircuitBreaker → HTTP.Client → Telemetry
  - Fail-fast config validation on start (raises `Aurinko.ConfigError` if credentials missing)
  - Structured startup summary logged at `:info` level
  - 5-second graceful shutdown timeout per child

#### Configuration (expanded)

- `Aurinko.Config` extended with new validated keys (all via `NimbleOptions`):
  - Cache: `:cache_enabled`, `:cache_ttl`, `:cache_max_size`, `:cache_cleanup_interval`
  - Rate limiter: `:rate_limiter_enabled`, `:rate_limit_per_token`, `:rate_limit_global`, `:rate_limit_burst`
  - Circuit breaker: `:circuit_breaker_enabled`, `:circuit_breaker_threshold`, `:circuit_breaker_timeout`
  - Telemetry: `:attach_default_telemetry`
  - `Config.merge/2` utility for per-request config overrides

#### Developer Experience

- **Guides** — `guides/getting_started.md` and `guides/advanced.md` added to ExDoc
- **CI** extended — Credo strict, Dialyzer, and ExCoveralls added as required checks
  - Elixir 1.16 / OTP 26 and Elixir 1.17 / OTP 27 matrix
  - Lint, format check, and Dialyzer run as separate CI jobs
- New `mix` aliases: `lint`, `test.all`, `quality`
- `config/staging.exs` — staging environment config with JSON logging pre-configured
- `config/runtime.exs` — runtime config reading all settings from environment variables

### Changed

- `Aurinko.HTTP.Client` completely rewritten — previously a thin `Req` wrapper; now a full GenServer with the middleware pipeline described above
- `Aurinko.Telemetry` expanded from request-only events to 7 events covering the full request lifecycle, circuit breaker state changes, and sync completion
- `Aurinko.Config` schema extended; `load!/0` now strips unknown application env keys before validation to avoid conflicts with middleware config keys
- All API functions (`Email`, `Calendar`, `Contacts`, `Tasks`, `Webhooks`, `Booking`) now route through the full middleware pipeline automatically

### Fixed

- Dialyzer: removed unreachable `is_list` guard clause from `get_header/2` (Req 0.5 always returns headers as a map)
- Dialyzer: narrowed `@spec format/4` return in `JSONFormatter` from `iodata()` to `binary()`
- Dialyzer: removed `{:error, :rate_limit_exceeded}` from `RateLimiter` type and spec (function never returns it)
- Dialyzer: narrowed `@spec events/0` in `Telemetry` from `list(list(atom()))` to `nonempty_list(nonempty_list(atom()))`
- Webhook verifier: replaced `Plug.Crypto.secure_compare/2` (undeclared dependency) with a self-contained `:crypto`-based constant-time comparison

---

## [0.1.0] — 2025-06-01

### Added

- Initial release with Starter Boilerplate elxir app