# Core Plane

The core plane contains all processing logic. It is protocol-agnostic — it sees only
`MwKernel.Context` and `MwKernel.Message` regardless of which gateway or adapter is involved.

---

## mw_kernel — Shared Foundation

The only app with zero internal dependencies. Defines the contracts that all other apps use.

### MwKernel.Message

```elixir
defmodule MwKernel.Message do
  @type t :: %__MODULE__{
    id: String.t(),               # UUID v7
    type: String.t(),             # dot-notation: "transaction.payment"
    payload: map(),               # canonical data fields
    source: atom(),               # :gateway_api | :gateway_ws | :gateway_mobile
    metadata: map(),              # headers, version, custom tags
    inserted_at: DateTime.t()
  }
  @enforce_keys [:id, :type, :payload, :source]
  defstruct [:id, :type, :payload, :source, metadata: %{}, inserted_at: nil]
end
```

### MwKernel.Context

```elixir
defmodule MwKernel.Context do
  @type t :: %__MODULE__{
    trace_id: String.t(),         # OTel trace ID — universal correlation key
    tenant_id: String.t(),
    user: MwAuth.Identity.t() | nil,
    roles: [atom()],
    request: MwKernel.Message.t() | nil,
    response: MwKernel.Message.t() | nil,
    adapter: module() | nil,
    halted: boolean(),
    assigns: map(),
    errors: [MwKernel.Error.t()]
  }
  defstruct [
    :trace_id, :tenant_id, :user, :request, :response, :adapter,
    roles: [], halted: false, assigns: %{}, errors: []
  ]

  def halt(%__MODULE__{} = ctx), do: %{ctx | halted: true}
  def assign(%__MODULE__{} = ctx, key, value),
    do: %{ctx | assigns: Map.put(ctx.assigns, key, value)}
  def put_error(%__MODULE__{} = ctx, error),
    do: %{ctx | errors: [error | ctx.errors]}
end
```

---

## mw_auth — Authentication & Authorization

### Identity struct (set by plug on successful auth)
```elixir
defmodule MwAuth.Identity do
  defstruct [:user_id, :tenant_id, :roles, :auth_method, :api_key_id]
  # auth_method: :jwt | :api_key
end
```

### Auth flow
```
Request arrives at MwAuth.Plug
  │
  ├── "Bearer" prefix → MwAuth.JWT.verify(token)
  │     ├── Joken.verify_and_validate(token, signer)
  │     ├── check jti not in TokenStore revocation list
  │     └── {:ok, %Identity{}} | {:error, reason}
  │
  └── "ApiKey" prefix → MwAuth.ApiKey.verify(key)
        ├── extract prefix (first 8 chars)
        ├── ETS lookup by prefix → get hashed record
        ├── Argon2.verify_pass(key, hash)
        ├── check active + expiry
        └── {:ok, %Identity{}} | {:error, reason}

On success: context = %{context | user: identity, roles: identity.roles}
On failure: context = Context.halt(context) → 401 response
```

### RBAC
```elixir
defmodule MwAuth.RBAC do
  @spec authorize!(MwKernel.Context.t(), [atom()]) :: :ok | no_return()
  def authorize!(%{roles: roles}, required) do
    if Enum.any?(required, &(&1 in roles)) do
      :ok
    else
      raise MwKernel.Error.unauthorized("Insufficient role. Required: #{inspect(required)}")
    end
  end
end
```

---

## mw_router — Pipeline Orchestration

### Pipeline Order
```
Stage 1: MwAuth.Plug          → verify identity
Stage 2: MwRouter.RateLimiter → check token bucket
Stage 3: MwTransform.Inbound  → validate + map to canonical
Stage 4: MwRouter.RoutePlug   → ETS lookup, resolve adapter
Stage 5: MwRouter.Dispatcher  → call adapter via behaviour
Stage 6: MwTransform.Outbound → map response to client format
Stage 7: MwAudit.Plug         → write audit event (async)
```

Each stage can halt the pipeline. Halted contexts skip remaining stages and return the
first error.

### Circuit Breaker States
```
CLOSED (normal)
  │ N failures in T seconds
  ▼
OPEN (rejecting all calls)
  │ after reset_timeout
  ▼
HALF-OPEN (one trial call)
  │ success → CLOSED
  │ failure → OPEN
```

Default config (overridable per adapter in route_rules):
- `fuse_strategy: {:standard, 5, 10_000}` — 5 failures in 10s
- `reset_timeout: 30_000` — 30s before half-open trial

---

## mw_transform — Data Transformation

### Message Type Naming Convention
```
<domain>.<action>
  transaction.payment
  transaction.reversal
  account.balance
  account.statement
  file.settlement_upload
  report.daily_summary
```

### Mapping Rule Structure
```
rule = %{
  message_type: "transaction.payment",
  direction: :inbound,
  source_field: "fromAccount",
  target_field: "account_from",
  transform_fn: :rename
}
```

Supported `transform_fn` values:
- `:rename` — copy value, change field name
- `:cast_integer` — parse string to integer
- `:cast_decimal` — parse to Decimal (for amounts)
- `:default` — use default value if source missing
- `:drop` — omit field from output
- `:snake_case` — rename key to snake_case automatically

---

## mw_audit — Compliance Logging

### Audit Event Fields
```elixir
%MwAudit.Event{
  trace_id:          "01HXYZ...",
  tenant_id:         "tenant-abc",
  user_id:           "user-123",
  auth_method:       :jwt,
  message_type:      "transaction.payment",
  adapter:           "AdapterBanking",
  status:            :success,     # :success | :auth_failure | :rate_limited |
                                   # :circuit_open | :timeout | :transform_error
  duration_ms:       187,
  request_snapshot:  %{amount: 100, ...},   # sanitised — no PAN/CVV
  response_snapshot: %{status: "approved"}, # sanitised
  error:             nil,
  inserted_at:       ~U[2026-04-26 12:00:00Z]
}
```

### Sanitisation
Before writing to `request_snapshot` / `response_snapshot`, these fields are masked:
- `card_number` → `"****1234"`
- `cvv` / `cvc` → `"***"`
- `pin` → `"[REDACTED]"`

Sanitisation rules are configurable in `config/config.exs`:
```elixir
config :mw_audit, :redacted_fields, ["card_number", "cvv", "pin", "password"]
```
