Ctrl K
ComunidadGuías…

CHANGELOG.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Changelog

All notable changes to KiloCenter are documented in this file.

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

## [Unreleased]

## [2.1.0] - 2026-10-23

> **Upgrade note:** migration 000192 adds the tenant setting `is_snapshot_only_decoding`, off for
> every tenant, so an upgraded installation decodes exactly as before. The migration runs on
> start-up. To go back to 2.0.0, stop the services and run this release's
> `migrate -command=down -steps=1` before starting the previous release.

### Added

- **An endpoint can be pinned to a blueprint specification directly.** `EndPoint.blueprint_input`
  (field 34, message `BlueprintInput`) carries a specification on `CreateEndPoint`, and on
  `UpdateEndPoint` with `blueprint_input` in the update mask. KiloCenter checks it exactly as the
  decoder does and pins a snapshot of it; the endpoint has no device model, and its type EUI is
  the specification's `typeEui`. The response returns the snapshot, never the input. The request
  is refused with `INVALID_ARGUMENT` and changes nothing when the input comes with a
  `blueprint_id` or a `device_model_id` (`KC-GRPC-ERR-0AC`), when the decoder does not accept
  the specification, for example without uplink and downlink formats or with a repeated format
  id (`KC-GRPC-ERR-0AD`, with the reason), or when its `typeEui` is not 16 hexadecimal digits
  (`KC-GRPC-ERR-0AE`). An empty specification is the same as no input.
- **Snapshot-only decoding, per tenant and off by default.** `SetSnapshotOnlyDecoding`
  (administrators) and `GetSnapshotOnlyDecoding` (any role) switch and read the caller's tenant.
  While it is on, for the endpoints the tenant owns, roaming ones included:
  - an uplink is decoded only from the endpoint's pinned snapshot; an endpoint without one, or
    with a damaged one, is not decoded. When the setting cannot be read, it counts as on;
  - creating an endpoint, or changing its device model, pins the model's default blueprint, or
    is refused with `FAILED_PRECONDITION` (`KC-GRPC-ERR-0AF`) when there is none and no
    `blueprint_id` or `blueprint_input` is given;
  - a `blueprint_id` must be a blueprint of the requested `device_model_id` and, unless
    `device_model_id` is in `update_mask`, of the endpoint's current model; otherwise
    `INVALID_ARGUMENT` (`KC-GRPC-ERR-0B0`) instead of moving the endpoint to the blueprint's
    model;
  - `BulkAssignBlueprint` with an endpoint of another device model, or without one, is refused
    as a whole with `INVALID_ARGUMENT` (`KC-GRPC-ERR-0B1`);
  - an update that changes neither the model nor the blueprint is not affected.
  Each switch is recorded as a `tenant.snapshot_only_decoding.changed` event.
- **`PinCurrentBlueprints`** (Endpoint Manager) pins every endpoint the caller's tenant owns that
  has no snapshot to the blueprint it decodes with now: its model's default, else the blueprint
  of its type EUI. Only the snapshot is written, and only while the endpoint still has none; its
  model is not changed. The response lists each endpoint as `pinned`, `nothing_to_pin` or
  `error`; `dry_run` reports the same without writing. A pass is recorded as a
  `blueprints.pinned` event.

### Changed

- The decoder checks a specification with `blueprint.ParseAndValidate`, the same check that
  accepts `blueprint_input`. `CreateBlueprint` and `CreateDeviceModelWithBlueprint` still accept
  any valid JSON, as before.
- `BulkAssignBlueprint` reads every listed endpoint before it changes any. A read failure other
  than "not found" refuses the call with `INTERNAL` and changes nothing; it used to skip the
  endpoint.
- When the tenant setting cannot be read, `CreateEndPoint`, an `UpdateEndPoint` that changes the
  device model or the blueprint, and `BulkAssignBlueprint` are refused with `INTERNAL`
  (`KC-GRPC-ERR-0B2`) and change nothing, whether the setting is on or off.

### Fixed

- An uplink decoded from a snapshot that has no source blueprint is stored with an empty
  `blueprint_version_id` instead of the nil UUID.

## [2.0.0] - 2026-10-08

KiloCenter 2.0 is a major release. Upgrading from 1.x takes a few manual steps, described in
[Key material migration](GitBook/06-Operations/02-key-material-migration.md).

**Why this is 2.0 and not 1.4:**

- **New settings are required before the upgrade.**
  - Set a master key (`KILOCENTER_MASTER_KEY`). It encrypts your endpoint and base station keys
    at rest, and the services do not start without it.
  - When KC-Gateway, KC-Core and KC-Identity run on separate hosts or containers, also set a
    shared internal secret (`KILOCENTER_INTERNAL_AUTH_PEER_SECRET`).
  - Upgrade KC-Core and KC-Identity together.
- **Your stored keys are converted.** The upgrade re-encrypts every stored key under the master
  key, and after that the database can no longer be used with a 1.x release. Back it up first.
- **Parts of the API changed.**
  - The organization quota fields are removed.
  - Endpoint keys are masked on every read.
  - Every call now checks the user's role and answers `PERMISSION_DENIED` when the role does not
    cover it.
  - The web interface's WebSocket connection is replaced by gRPC-web server streams.

**Cleanup and bug fixes.** A major cleanup release:
- Dead code, unused settings and unused exported declarations are removed, along with the old
  System screen and the WebSocket transport.
- Large components are split into smaller, focused parts.
- Hard-coded values are moved into shared catalogs and configuration.
- More than fifty bugs are fixed across uplinks, downlinks, sessions, certificates and the web
  interface. They are listed under Fixed below.

**New in the web interface:**
- **Live updates.** Dashboards, lists and activity refresh within about a second of a message
  arriving, with no manual refresh.
- **Activity on every base station and endpoint.** One table shows each uplink with its radio
  metrics, payload and decoded values inline. Rows expand to the receiving stations, their
  subpackets and the ulData message.
- **Traffic.** Uplinks show decoded values when the endpoint has a blueprint. Downlinks can be
  queued, revoked and followed to delivery.
- **Blueprints.** A blueprint can be assigned from the endpoint's page.
- **Logs.** An error center and an event log with filters replace the old activity feed, and
  the dashboard shows an Alerts card.
- **Base stations.** A map of the stations, plus certificate status and expiry. A station's
  certificate bundle can be downloaded, and the server certificate renewed without losing the
  names stations connect by.
- **Users and roles.** Administrators grant roles. A refused password shows the reason, and API
  keys can be created in the community edition.

**New in the service center:**
- **Protocol conformance.** Uplinks, downlinks, attach and detach follow BSSCI v1.0.0 and SCACI
  v1.0.0 end to end.
- **Sessions.** Base station and Application Center sessions survive a restart and resume.
- **Uplinks.** One uplink received by several base stations is delivered once, listing every
  receiving station.
- **Downlinks.** Downlinks are queued, revoked, expired and reported to the Application Center
  that queued them.
- **Tenant isolation and security.**
  - Roles are enforced on every call, and secrets are audited before they are handed out.
  - Base station certificates are bound to their station.
  - SCACI sessions, queue ids and results stay within their organization.
  - Calls between the services are authenticated.
- **New API fields.** The endpoint's latest RSSI, SNR and equivalent SNR, the downlink accept
  and acknowledge times, the base station certificate expiry, and a password policy.
- **New MQTT fields and events.** `dlOpen`, `responseExp` and `dlAck` on uplinks, delivery details
  on downlink results, more `command/down` options, and `event/downlink_queued`.

> **Upgrade note:** migration 000189 clears the `ref` of every later downlink that repeated a
> ref an organization had already used for the same endpoint; only the earliest keeps it. A
> client still waiting for the result of such a repeated downlink receives that result without
> `ref`, so it cannot match it to its command.

> **Breaking:** organization quotas are gone. `can_have_base_stations`,
> `max_base_station_count` and `max_endpoint_count` no longer exist on
> `CreateOrganizationRequest`, `UpdateOrganizationRequest` or `Organization`
> (the field numbers and names are reserved), the UI no longer shows or edits
> them, and migration 000154 drops the columns. They were published but never
> enforced; a deployment that needs entitlements enforces them in the platform
> above the service center. Clients that still send the fields get them
> ignored as unknown fields; clients that read them must drop the code.

> **The user role switches are enforced, and existing accounts keep their
> access.** Every API call except sign-in, registration and the public release
> and onboarding calls needs a signed-in user whose roles cover it (Admin,
> Tenant Manager, Base Station Manager, Endpoint Manager); other calls return
> `PERMISSION_DENIED`. Migration 000176 grandfathers every account and active
> organization membership that exists when it runs: they gain Base Station
> Manager and Endpoint Manager, which reproduces the unrestricted data access
> they had, members who could manage members keep Tenant Manager, and nobody
> becomes an administrator. The migration records what it changed, so
> migrating down restores the previous switches. Accounts created after the
> upgrade, whether by self-registration, invitation or a first OpenID Connect
> sign-in, start without roles until an administrator grants one.
> Service-account API keys act as Base Station Manager and Endpoint Manager in
> their own organization only.
>
> **Upgrade note:** KC-Core no longer starts without `identity.address`, a stack
> run with authentication disabled no longer serves data calls, and the internal
> `IdentityInternalService.GetUserMembership` RPC is replaced by `GetUserRoles`,
> so KC-Core and KC-Identity must be upgraded together.

### Changed
- Endpoint keys are masked on every read. `GetEndPoint`, `ListEndPoints` and
  the `CreateEndPoint` and `UpdateEndPoint` responses return `nwk_sn_key` and
  `app_key` empty, with `nwk_sn_key_set` and `app_key_set` saying whether each
  is stored. An Endpoint Manager or Admin reads a key by calling `GetEndPoint`
  with `reveal_keys` (`ENDPOINT_KEY_NWK_SN_KEY`, `ENDPOINT_KEY_APP_KEY`), and
  every reveal writes an `endpoint.keys_revealed` audit event naming who
  revealed which key of which endpoint, never the key. Clients that read keys
  from endpoint records must ask for them explicitly. In the web interface the
  Configuration tab shows each key as set or not set, and the edit dialog
  shows a stored key masked and empty, with a Reveal button that fetches it
  and a Copy button that works once it is revealed or a new key is entered.
  Leaving a key field empty keeps the stored key. A stored application key is
  removed with the field's Remove action, confirmed first and applied on save
  (`UpdateEndPoint` with `app_key` in the mask and no value); the network
  session key is mandatory, so it can be replaced but never removed. Every
  removal of a stored key writes an `endpoint.keys_removed` audit event naming
  who removed which key of which endpoint, never the key.
- The web interface has no System screen. The Application Center control
  plane, statistics and analytics, integrations, capabilities and the
  diagnostics bundle are API calls for the software that connects to the
  service center, each with its own role
  (`GitBook/05-Security/04-users-and-roles.md`); the dashboard shows Endpoint
  Managers the Application Center status and Base Station Managers the
  certificate expiry of their stations.
- A base station's and an endpoint's **Activity** tab is one table in the
  Events Log columns (Time, Event type, opId, Scope, Severity, Summary)
  instead of a terminal-style timeline. Events read as in the Events Log and
  expand to their data; uplinks read as `ulData` with their `opId`, the
  endpoint (on a station) or the receiving stations (on an endpoint) and
  their radio metrics, and expand to the receptions, the `ulData` message and
  the blueprint-decoded payload, which the Traffic tables now show too. Paging
  only offers pages the feed's cursor has reached, so a page number no longer
  shows another page's rows. The separate device **Logs** tab is gone; an
  endpoint's attach, detach or delete failure shows on every tab.
- The dashboard has no Platform Activity feed. The **Alerts** card reads like
  the Events Log and carries the live-connection indicator and a link to the
  Events Log, where the full platform history is.
- `ListBaseStationMessages` takes the uplink filters `ListMessages` has
  (`duplicate`, `dl_open`, `profile`, `mode`), and `BaseStationMessage`
  carries `dl_open`, `res_exp`, `dl_ack`, `op_id` and `format`, so a base
  station's Traffic tab lists the station's uplinks with the same columns and
  filters as the Traffic section. A base station manager sees them there.
- The Helm value `kcIdentity.config.auth.registrationEnabled` is removed. No
  template read it, so KC-Identity always ran with self-registration off, which
  is unchanged.
- A refused password is answered with its reason: `CreateUser`,
  `UpdateUserPassword` and `ChangePassword` return InvalidArgument with the
  password policy for a weak password (was Internal "failed to ...") and
  `ChangePassword` returns InvalidArgument "current password is incorrect"
  for a wrong current password.
- API keys can be created in the community edition (`CreateApiKey` returned
  "service not configured" there).
- The base station uplink export (`ExportBaseStationMessages`) carries the
  SCACI §3.8.1 ulData fields in both formats under the same camelCase names:
  `id`, `opId`, `epEui`, `bsEui`, `rxTime`, `rxDuration`, `packetCnt`, `snr`,
  `rssi`, `eqSnr`, `profile`, `mode`, `format`, `dlOpen`, `responseExp`,
  `dlAck`, `duplicate`, `userData`, `subpackets`. EUIs are hex, `opId` is a
  decimal string, `rxTime` is RFC 3339 in UTC with nanoseconds and `userData`
  is hex; before, the JSON export wrote EUIs, `opId` and `rxTime` as numbers
  that JavaScript rounded, and `userData` as base64, and the CSV lacked most
  fields and used snake_case headers.
- Every 64-bit identifier and nanosecond timestamp in the gRPC API is
  delivered to JavaScript clients as a string (`jstype = JS_STRING`): downlink
  `que_id`, BSSCI/SCACI `op_id` fields, `rx_time`/`tx_time`, the SCACI session
  `last_op_id_*` counters and `ReleaseInfo.sc_eui`. Generated JavaScript
  getters for these fields now return strings; Go, Python and other clients are
  unaffected.
- `ListScaciSessionsRequest` gains `can_resume_filter` (explicit presence) so a
  client can ask for non-resumable sessions; the implicit `can_resume` field is
  deprecated and still means "resumable only" when set.
- Uplink duplicates are recognized by the base station's reception time
  (`rxTime`) instead of the time they reach the service center, so a ulData a
  base station resends after a session resume merges into the original message.
- A failed uplink delivery to an Application Center or MQTT is retried until it
  succeeds, with an exponential backoff capped by the new
  `protocol.delivery.max_backoff` (default 5m); only deliveries that can never
  succeed (unknown or unconfigured channel, message gone) are parked.
- An over-the-air attach restarts the endpoint's packet counter, and the stored
  counter otherwise never moves backwards.
- The configured `protocol.roaming.cache_*` and `enable_audit_trail` settings
  now take effect, and every reception is judged against the tenant of the base
  station that received it.
- An upgrade from a release that stored plaintext key material stops at
  migration 000143 until the `rekey` command has converted every key: both
  `migrate up` and service start-up refuse to continue and leave the schema
  clean at 143. The Helm chart's pre-upgrade hook already runs `rekey` there.
- The Helm `rekey` hook reads its own hook-scoped Secret, decrypts rows sealed
  with the retired published development passphrase by default
  (`rekey.allowPublishedDevelopmentKey: true`, since the chart never set
  `KC_ENCRYPTION_KEY`), and can export retired `endpoint_keys` rows to a
  persistent volume (`rekey.archiveExport.existingClaim`).
  `rekey.legacyPassphraseSecretKey` is replaced by `rekey.legacyPassphrase`.
- SCACI session resume follows §3.3.1: `snAcOpId` is the lowest Application
  Center operation the service center must know and `snScOpId` the highest
  service center operation the Application Center has seen; a resumed session
  accepts the Application Center's reissued operations with their original ids
  and replays the service center's own uncompleted ones.
- A new SCACI session of an Application Center supersedes its earlier session
  and closes the older connection; a session only becomes usable after
  `conCmp`, and the TLS handshake plus connect must finish within
  `protocol.connection_establishment_timeout`.
- Every service center operation sent over SCACI is recorded for resume before
  it is written; a broadcast reports each failed session and continues with the
  rest.
- Every BSSCI and SCACI frame write is bounded by the new
  `protocol.socket_write_timeout` (default 10 s), so one stalled peer no longer
  blocks delivery to the others.
- A base station may complete its own operations out of order: a completion for
  an operation that is still open is accepted, and only new operations must use
  a higher id. Stations that sent their next uplink before completing the
  previous one were disconnected on every such overlap.
- BSSCI sessions survive a service center restart or crash and can be resumed.
- `/health/ready` reports only what KC-Core needs to serve its API (PostgreSQL
  and the internal gRPC server); `/health` still lists every component, and the
  BSSCI/SCACI rows no longer open connections to the listeners.
- KC-Core and KC-Identity trust gateway identity headers only from a caller that
  presents `internal_auth.peer_secret` when one is configured (set the same value
  on KC-Gateway, KC-Core and KC-Identity; empty keeps the previous behavior).
- The service center assigns every downlink queue id; an Application Center's own
  `queId` is kept per organization while its downlink is in flight and returned
  in its `dlDataRes`, so two tenants, or two organizations of a tenant, can use
  the same id.
- A downlink that is persisted is acknowledged to the Application Center even if
  the immediate hand-off to the base station fails; it is delivered later.
- `RevokeDownlink` revokes a pending downlink immediately (status `revoked`) and
  answers NotFound unless the queue id belongs to the requested endpoint and
  organization. `ListDownlinkQueue` lists the request organization only.
- A downlink the base station refuses is marked failed and reported as
  `invalid` to the Application Center and on MQTT `event/downlink_result`.
- A ulData with a malformed `subpackets` object, or with more than 200 bytes of
  `userData` (the radio payload limit), is answered with a BSSCI protocol error
  (EPROTO) and not stored. An `att` or `det` with a malformed `subpackets`
  object is refused the same way (BSSCI §3.10.1) instead of being accepted
  without it.
- When several base stations receive the same uplink, the endpoint's downlink
  window is filled with one queued downlink, not one per station.
- SCACI ulData reports `duplicate: true` when an endpoint reuses a packet counter
  it already sent outside the duplicate window; the uplink is still stored and
  delivered. Migration 000163 adds `messages.packet_cnt_reused`.
- SCACI `baseStations[].dlRxSnr`/`dlRxRssi` ride only the first uplink after the
  endpoint's DL RX status report instead of every later uplink.
- New downlink queue ids are at most 2^53-1, so JavaScript and other JSON clients
  read them exactly.
- MQTT `command/down` payloads are limited to 200 decoded bytes, the radio
  downlink limit, instead of 256 KB.
- Over-the-air attachment follows the radio specification: the attach signature
  is checked over the 16-byte initialization vector with a 4-byte attach counter
  (it used 15 bytes, so a real endpoint's over-the-air attach was refused), other
  base stations receive the session key instead of the pre-shared key, an
  attach response omits `shAddr` when the station assigned one, and concurrent
  identical attaches are served once.
- After an over-the-air detach the other base stations are told to drop the
  endpoint, and Application Centers receive `epStat` for attachments and
  detachments made through the service center as well.
- SCACI byte fields travel as arrays of numbers in both directions, as the
  specification defines them; binary values are still accepted. Out-of-range
  numbers are refused with code 34 instead of being truncated, a `dlDataQue`
  without `cntDepend` or `userData` is refused, `prio` is accepted in any numeric
  width, and JSON-framed Application Centers are served (replies stay
  MessagePack).
- `ListScaciQueues` lists the request organization only.
- A downlink queued ahead of time goes to the base station that received the
  endpoint's latest uplink with the best SNR, a roaming station of another
  tenant included, or, before the endpoint has been heard, to the station it
  was attached or propagated through. When that station is offline or cannot
  transmit, the downlink waits for the endpoint's next downlink window instead
  of going to a station that never hears the endpoint. A downlink for an
  endpoint no station has heard or attached yet waits for the window of its
  first `dlOpen` uplink too, instead of going to an arbitrary bidirectional
  station of its tenant. `QueryDLRXStatus` asks the same station.
- A downlink for an endpoint registered without `bidi` is refused: SCACI
  answers the `dlDataQue` with an error (code 95) and `SendDownlink` returns
  FailedPrecondition (KC-GRPC-ERR-280). No `dlDataQue` is sent to a base station
  whose session is not bidirectional.
- When several base stations report the same telegram, its downlink window goes
  to the first bidirectional one, so a receive-only station that reported it
  first no longer leaves the window unused. Migration 000165 adds
  `messages.dl_window_claimed`.
- When a base station starts a new session instead of resuming, the downlinks
  it held are queued again: a new session discards the station's state.
- A downlink waits at most `protocol.downlink_expiry.lifetime` (default 24h)
  for a downlink window. After that it is marked expired and reported as
  `expired` to the Application Center that queued it, on MQTT
  `event/downlink_result` and in `GetDownlinkResults`.
  `protocol.downlink_expiry.sweep_interval` (default 5s) and `batch_size`
  (default 100) tune the sweep; Helm `kcCore.config.protocol.downlinkExpiry`.
  A downlink a base station still holds expires the same way, and the base
  station is asked to drop it with `dlDataRev`. Its confirmation, and a
  `dlDataRes` for a downlink that already expired or was revoked, leave the
  reported outcome unchanged and are not reported again.
- SCACI `dlDataRev` revokes every downlink scheduled for the packet counter and
  nothing else: a downlink queued without packet counters is no longer revoked
  by whatever counter the Application Center names. With nothing scheduled for
  the counter, the revoke completes instead of answering with an error.
- `dlDataRes` reaches only the Application Center that queued the downlink
  (the one of the downlink's organization whose `dlDataQue` queued it), not
  every Application Center of the tenant; results of downlinks queued through
  gRPC or MQTT stay on those channels. The queue row stores that Application
  Center (migration 000184 adds `downlink_queue.ac_eui` and fills it for
  existing downlinks from their `dlDataQue` records), so a result still
  reaches it when the operation log could not record the `dlDataQue`. A
  downlink queued before the upgrade whose records name no Application Center,
  or several, is reported to none; the upgrade preflight lists them.
- While an Application Center is disconnected but may still resume its SCACI
  session, the service center records the `ulData`, `epStat` and `dlDataRes` it
  would have sent under the session's next operation ids. On resume they are
  reissued after `conCmp`, in operation id order and before any new operation,
  with ids continuing from before the connection loss; a new session discards
  them, and a resume of a session that is no longer held is refused. Held
  sessions survive a service center restart. The new
  `protocol.scaci_resume_max_pending_operations` (default 10000; Helm
  `kcCore.config.scaci.resumeMaxPendingOperations`) bounds what a session holds:
  the operation that would exceed it ends the session's resumability.
- An Application Center's `queId` may be any non-zero 64-bit value; values at or
  above 2^63 were refused with code 34, and `dlDataRes` returns the exact id.
  Migration 000166 changes `downlink_queue.ac_que_id` to NUMERIC(20,0).
- `GetDownlinkResults` and `ListDownlinkQueue` report the result `revoked` for a
  revoked downlink instead of an empty result.
- Shutdown waits for the downlink results still being forwarded to Application
  Centers.
- An endpoint's attachment status is the service center's decision.
  `AttachEndPoint`, `DetachEndPoint`, `UpdateEndPoint` with `status`,
  `CreateEndPoint` with `pre_attach`, a SCACI `reg` with `preAttach`, a SCACI
  `dereg`, and an over-the-air attach or detach set it, and only the call that
  changes it sends `epStat` to the owner's Application Centers, once: a
  `dereg` of an endpoint already detached announces nothing. A base station's
  attPrp or detPrp completion only records that station's confirmation: it no
  longer sets the status, sends `epStat` or publishes MQTT, so a late
  completion cannot undo a newer decision. An endpoint attached while no base
  station is connected is sent to each station when it connects.
- `CreateEndPoint` ignores the submitted endpoint's `status`: it is detached
  unless `pre_attach` attaches it. A pre-attached endpoint is stored together
  with its attachment, or not at all: one whose network key cannot be sent to
  the base stations is refused (KC-GRPC-ERR-10B) and stores nothing, so the
  same create can be sent again.
- A base station that connects or reconnects is sent attPrp only for the
  endpoints that are attached, not for every endpoint of its tenant. A base
  station that resumes its session kept the endpoints it held, so it is also
  sent detPrp for every endpoint whose detachment was decided after its
  connection was lost; one that starts a new session is not. The time of each
  decision is recorded in the new `attachment_changed_at` column (added as
  `status_changed_at` by migration 000168, backfilled from the last
  over-the-air detach of detached endpoints, and renamed by migration 000170),
  so a late base station report no longer moves it, and `last_detach_time`
  keeps only the reception time of the last over-the-air detach.
- `UpdateEndPoint` with `status` in `update_mask` attaches (`attached`) or
  detaches (`detached`) the endpoint through `AttachEndPoint` or
  `DetachEndPoint`, so the base stations are sent the change and Application
  Centers receive one `epStat`; the status was written directly before. An
  unchanged status changes nothing, `attaching` is refused with
  KC-GRPC-ERR-315 ("status must be attached or detached"). No other endpoint
  update changes the attachment status.
- `AttachEndPoint` and `DetachEndPoint` of an endpoint the tenant does not
  have answer not found (KC-GRPC-ERR-101) instead of a failed attach or
  detach.
- `CreateEndPoint` with an `ep_class` other than `A` or `Z` is refused with
  KC-GRPC-ERR-314 instead of failing in storage, and every endpoint is stored
  with the class its `bidi` flag implies, whatever class the writer passed.
- MQTT `event/attach` and `event/detach` are published once for every
  attachment and detachment the service center decides, over the air or made
  in KC-Web, through the API or by an Application Center; only an over-the-air
  one carries `bsEui`. An over-the-air attach is recorded when the base
  station completes it (`attCmp`).
- MQTT `event/downlink_queued`, `event/downlink_rejected` and
  `event/downlink_result` are published at QoS 1 (at least once) instead of
  QoS 0, so a platform that settles its commands on them no longer loses an
  outcome the broker dropped. An outcome can now arrive more than once; treat a
  repeat for the same `queId` (or, for `downlink_rejected`, the same `ref`) as
  the same outcome. `event/attach` and `event/detach` stay at QoS 0.
- Every SCACI request without one of its mandatory fields is refused with
  `scaci.error.missing_mandatory_field` (code 22). This replaces the per-field
  checks, including `scaci.error.missing_packet_cnt`, which no longer exists:
  a `dlDataRev` without `packetCnt` now gets `missing_mandatory_field`.
- SCACI requests are decoded once. A JSON frame's mandatory fields are found
  by the same key matching as its other fields, and a frame with more than one
  defect is answered with the first defect the decoder meets; a frame with a
  single missing or invalid field is refused as before.
- A base station's frames no longer start one database write each: the
  session's operation counters are written by one writer at a time, which
  picks up the newest counters, and a slow write no longer holds back the next.
- Negative values in SCACI 64-bit unsigned fields (`epEui`, `bsEui`, `queId`,
  `acEui`) are refused with `scaci.error.field_out_of_range` (code 34) instead
  of wrapping into a different identifier.
- A SCACI `dereg` revokes the endpoint's downlinks through the revoke path, so a
  downlink a base station already holds is revoked there with `dlDataRev`
  instead of only in the store.
- `GetScaciStatus` requires an organization and counts the pending downlink
  operations of that organization only.
- The Helm chart rolls KC-Core and Mosquitto out with the `Recreate` strategy.
  A rolling update started the new KC-Core beside the old one, and its
  start-up marked the old pod's live BSSCI sessions disconnected; both pods
  also mount a `ReadWriteOnce` volume that attaches to one node at a time.
- A configuration with an empty `certificates.certgen_path`, `certs_dir` or
  `temp_dir`, or a `server_validity_days` or `cleanup_interval_min` of zero or
  less, is refused at start-up instead of silently replaced by the default;
  leave a setting out to get its default.
- `protocol.strict_org_resolution=true` with a SCACI listener requires
  `protocol.scaci_cert_tenant_mapping=true` in every edition, checked when
  KC-Core loads its configuration.
- KC-Identity caches the default organization of a tenant for
  `general.org_cache_ttl_minutes`, as KC-Core does, instead of querying the
  database on every request; both binaries share one organization resolver.

### Added
- **An MQTT downlink command can carry a deadline.** `command/down` accepts an optional `expiresAt`
  (RFC 3339). The downlink waits for its endpoint's downlink window until
  `protocol.downlink_expiry.lifetime` has passed or until `expiresAt`, whichever comes first. A
  command whose `expiresAt` has already passed queues nothing and is refused on
  `event/downlink_rejected` with `mqtt.command.expired`.
- **An MQTT command's `ref` is accepted once.** A `command/down` whose `ref` already queued a
  downlink for the same endpoint of the organization queues nothing and publishes nothing, also
  after the first downlink ended and after the repeat's deadline passed. A client can republish a
  command it is not sure arrived. The ref is compared before the payload and the endpoint are
  checked, so a repeat is not refused when the endpoint was deleted or lost its downlink
  capability since. Migration 000189 adds a unique index on the ref of an organization's
  endpoint; refs repeated before the upgrade stay on their earliest downlink only.
- MQTT `event/up` carries the whole uplink: `epEui`, `eqSnr`, `rxDuration`,
  `format`, `profile`, `mode`, `subpackets`, every receiving base station in
  `baseStations` (with `dlRxSnr`/`dlRxRssi` when reported), `duplicate` and
  `packetCntReused`, and the decode outcome in `decodeStatus`,
  `decodeErrorCode` and `blueprintTypeEui`. The earlier fields are unchanged.
- `AuthSettings.password_policy` (field 11, new message `PasswordPolicy`): the
  minimum and maximum length and the letter and digit requirements every new
  password must meet, so clients can state the rules before a password is
  refused.
- `user.password_changed` audit events for a user's own password change and an
  admin's reset, filed under the platform tenant with the actor as the event's
  user and the user whose password changed as `userId` in its data.
- `user.registered` audit events for self-registration, filed under the
  platform tenant with the new user as the event's user and as `userId` in its
  data; no credential enters the event.
- `EndPoint.last_rssi`, `last_snr`, `last_eq_snr` (fields 28-30): the latest
  reception of the endpoint, absent until it is heard; `EndPoint.serving_bs_eui`
  (field 31, `GetEndPoint` only): the base station a downlink queued now would
  go to. The endpoint detail shows them with its MIOTY configuration (short
  address in hex, class, counters, type EUI, carrier offset, radio flags and
  whether each key is set).
- `DownlinkMessage.accepted_at` (field 29): when the base station in `bs_eui`
  accepted the downlink with dlDataQueRsp (BSSCI §3.12); unset until a station
  accepts it and cleared when the downlink returns to pending. The endpoint's
  downlink queue shows the holding station and its acceptance, and both
  downlink tables list every payload with the packet counter it is valid for.
- `BaseStationMessage.dl_open`, `dl_ack` and `res_exp` (fields 23-25): the
  uplink flags of a base station's activity, as `Message` already carries them
  for an endpoint's; the web activity shows them with the endpoint, eqSNR and
  the payload in hex.
- `BaseStation.certificate_expires_at` (field 24, optional): the expiry of the
  certificate the service center issued for the station, on both the list and
  the detail, so the base station list shows it.
- `GetServerCertificateStatusResponse.renewal_names` (field 3): the names a
  server certificate issued now carries, subject first. The Certificates page
  lists them before a renewal is confirmed.
- `certificates.server_names`: further DNS names and IP addresses the server
  certificate carries; certgen takes them through `-san`.
- `DownlinkMessage.endpoint_acked_at` (field 28): when an uplink with `dlAck`
  acknowledged the downlink sent in the endpoint's previous window; the downlink
  results in the web interface show it as "Acknowledged by device". Migration
  000164 adds `downlink_queue.endpoint_acked_at`.
- `Message.op_id` (field 24): the BSSCI ulData operation id of the first
  reception, for joining an uplink to its event-log rows.
- `EndPoint.reattach_pending` (field 27, read-only): an edit changed an
  attached endpoint's attach propagate parameters (BSSCI §3.8.1) after the
  last attach propagate a base station completed, so the stations still hold
  the earlier profile until the endpoint is attached again. The web interface
  marks such an endpoint "Configuration pending re-attach". Migration 000173
  adds `endpoints.profile_changed_at`.
- MQTT `event/up` carries `dlOpen`, `responseExp` and `dlAck`.
- MQTT `event/downlink_result` carries `bsEui`, `txTime` and `packetCnt` when
  `result` is `sent`.
- MQTT `command/down` accepts `prio`, `format`, `responsePrio`, `dlWindReq`,
  `expOnly`, `dlRxStatQry`, counter-dependent `entries`
  (`[{"packetCnt":N,"data":"base64"}]`) and a correlation `ref`; an empty `data`
  queues a pure acknowledgement. Existing `{"data","confirmed"}` commands queue
  the same downlink as before.
- MQTT `event/downlink_queued` (the queue id of an accepted command) and
  `event/downlink_rejected` (a stable `code` and `message`), both echoing `ref`.
- MQTT `event/downlink_result` carries the `ref` of the `command/down` that
  queued the downlink on every result, so your application can match results
  to its commands without keeping its own list of queue ids. A `ref` longer
  than 128 bytes is refused with `mqtt.command.ref_too_long`. Migration 000187
  adds `downlink_queue.ref`.
- MQTT `event/downlink_result` with `"result": "acknowledged"`: published once
  when the endpoint confirms, with the acknowledgement flag of its next uplink,
  that it received a sent downlink. It carries the downlink's `queId`, your
  `ref` and the acknowledged window in `packetCnt`. When the endpoint's packet
  counter restarted, the downlink sent last in that window is the one
  acknowledged. Application Centers keep reading the flag from `ulData`, as
  SCACI defines no such result.

### Removed
- Organization and tenant quota fields, columns and UI (`can_have_base_stations`,
  `max_base_station_count`, `max_endpoint_count`, `tenants.max_basestations`,
  `tenants.max_endpoints`).
- `protocol.delivery.max_attempts` (Helm `delivery.maxAttempts`); old
  configuration files that still set it load unchanged.
- Unused exported Go declarations: `storage.ErrStorageNotAvailable` (KC-DB),
  the log fields `FieldHandshakeComplete`, `FieldMaxAllowed`,
  `FieldOperationID` and `FieldTotalSessionsCamel` (`pkg/logger` and the
  KC-Core `logger` package), `bssci.LogBSSCIUnsupportedOperationTypeStartEvent`
  and the KC-Core internal `ErrNilBackgroundWork`.
- gRPC-web over WebSocket, and with it the `grpc.web.enable_websockets` setting
  (`config.DefaultGRPCWebEnableWebsockets`, `GRPCWebConfig.EnableWebsockets`,
  log field `FieldWebsockets`). Every RPC is unary or server-streaming, so
  gRPC-web clients use the default HTTP transport, as KC-Web does. KC-Gateway
  had stopped routing WebSocket upgrades when it began stripping hop-by-hop
  headers; KC-Core's optional gRPC-web listener (`grpc.web.enabled`) now
  serves HTTP only too. Configuration files that still set
  `enable_websockets` load unchanged; the key is ignored.

### Security
- A secret is handed out only once its audit record is written. An endpoint
  key reveal (`GetEndPoint` with `reveal_keys`), a base station private key
  download (`DownloadBaseStationCertificate` or `DownloadCertificate` with
  `cert_type=key`) and a new API key (`CreateApiKey`) answer `INTERNAL`
  without the secret when the event store cannot take the record, instead of
  handing it out unrecorded. A private key refused this way stays stored and
  can be downloaded again once the event store is back; an API key refused
  this way is stored only as a hash, so nobody can use it. Every private key
  download now writes a `certificate.private_key_downloaded` audit event
  naming who downloaded the key of which station, never the key. A second
  download of a key that another download is taking at that moment is refused
  at once instead of waiting.
- Roles are enforced in the service center and reflected in KC-Web. A user
  holding only the endpoint manager role could register a base station and
  receive its private key, and a self-registered user without any role could
  list the tenant's endpoints, read their network session keys and see
  security and system events. Each RPC now declares the role it requires
  (`GitBook/05-Security/04-users-and-roles.md` lists them); endpoint keys go
  only to endpoint managers, base station certificates and keys only to base
  station managers, and security, system, audit and error events, alerts and
  the diagnostics bundle only to administrators. Event lists show only the
  categories a user's roles cover. A role change applies within
  `grpc.rbac_role_cache_ttl_seconds` without a new sign-in. A deactivated
  administrator no longer passes the server-admin check. `GetProfile` returns
  the caller's effective roles (`UserProfile.roles`).
- A base station's private key leaves the service center once, as the
  certificate dialog already promised: `DownloadBaseStationCertificate` with
  `cert_type=key` returned the decrypted stored key on every call, and
  `DownloadCertificate` served a bundle's key file to every download until the
  bundle was cleaned up. Both paths now take the stored key under a row lock,
  so exactly one download, even among concurrent ones, receives it; every later
  one is refused, and the bundle's key file is removed with it. A key that
  fails to decrypt (for example after a master key change) stays stored, and a
  bundle whose key was superseded by a newer issuance is refused.
- Installation-wide audit events (users created, changed or deleted,
  organizations and API keys deleted, server certificates generated or
  renewed) are filed under the platform tenant `general.tenant_id`, and only
  administrators read them: the event list, event stream, error groups and the
  base station and endpoint activity timelines all apply the caller's
  readable event categories.
- Integrations are managed by administrators only, and their settings are
  write-only: `GetIntegration`, `ListIntegrations` and the create and update
  responses name each configured setting with the value `configured` instead
  of returning credentials such as HTTP headers or broker passwords.
- An administrator's password reset (`UpdateUserPassword`) signs the user out
  everywhere: every refresh token issued before the reset is revoked, as a
  user's own password change already did. A reset of a compromised account
  left the attacker's session renewable. The reset reports an error when the
  sessions cannot be revoked.
- A base station's answer to a `dlDataRev` ends only a downlink that station
  holds. A late confirmation or refusal from a station that no longer held the
  downlink revoked it while another station held it, so that station's
  transmission was reported to nobody. The revoke of a downlink still in the
  queue is unchanged.
- An Application Center's `queId` names one downlink of its organization until
  the downlink's result. It was unique within the tenant for good, so a
  `dlDataQue` failed with `EEXIST` when another organization of the tenant had
  used the id, which disclosed that organization's id, and an id could never be
  used again after its downlink ended. Migration 000172 scopes the id to the
  organization's downlinks in flight; two in-flight downlinks of one
  organization still refuse to share an id. The down migration refuses while an
  id repeats within a tenant.
- A base station's client certificate is bound to the station it claims in
  every edition, not only with organization enforcement. Before, a community
  edition base station with any certificate the CA signed could connect as
  another registered station and receive its endpoints' network session keys
  and queued downlinks. A certificate naming another station, or one that is
  not the certificate pinned for the station, is now refused as if the station
  were not registered. A station registered without a certificate is pinned to
  the first certificate that names it.
- The unauthenticated CE onboarding call completes onboarding exactly once:
  two concurrent calls on an installation that had not finished onboarding
  both succeeded, and the later one overwrote the company name.
- A SCACI `regCmp` reaches only the registering Application Center's own
  tenant. A `reg` is recorded before it is validated, so a refused `reg`
  naming another tenant's EUI followed by `regCmp` found that tenant's
  endpoint and, when it was pre-attached, attached it and propagated its keys.
- A SCACI `deregCmp` completes only a `dereg` the service center answered with
  `deregRsp`, and detach propagation sends `detPrp` only for an endpoint of
  the deregistering tenant. A `dereg` is recorded before it is validated, so a
  refused `dereg` naming another tenant's EUI followed by `deregCmp` sent that
  endpoint `detPrp` on every base station; any recorded operation carrying an
  `epEui` did the same. A `deregCmp` for a `dereg` answered with an error, one
  not answered yet or already completed, another operation or an unknown
  `opId` is now answered with `scaci.error.unexpected_deregister_complete`
  (EPROTO) and neither revokes downlinks nor sends `detPrp`. The completion
  reads the `dereg` from the operation log, so a `deregCmp` after a resume
  completes it too.
- A SCACI session is resumed only by the Application Center it belongs to:
  the same organization and `acEui`. The resume compared only the tenant, so
  an Application Center of another organization of the same tenant that
  presented a disconnected session's `snAcUuid` took that session over under
  its organization, closed its connection, received the downlink results
  held for it and queued downlinks in its name. Such a connect now starts a
  new session of its own organization.
- A new SCACI session supersedes only the earlier sessions of its own
  tenant, organization and `acEui`. An Application Center of another
  organization of the same tenant that claimed an `acEui` in use closed the
  owner's live connection, discarded what was held for its disconnected
  session and ended that session's resumability. Each organization now keeps
  its own session of the `acEui`: migration 000171 allows one live session
  per Application Center of an organization instead of one per tenant and
  `acEui`, which failed such a connect with an internal error while the owner
  was live and locked the owner out while it was offline; and a resume looks
  up only the connecting Application Center's session, so another
  organization's session of the owner's `snAcUuid` no longer defeats the
  owner's resume. The down migration refuses while an `acEui` is live in more
  than one organization of a tenant.
- A SCACI `dlDataRes` reaches only the Application Center that queued the
  downlink. It was sent to the Application Center whose `dlDataQue` last used
  the same queue id anywhere in the tenant, so another Application Center,
  also of another organization, that reused the queue id received it, and
  every session of the queuer's `acEui` in any organization of the tenant,
  connected or held for resume, received it too. A `dlDataQue` is now
  recorded with the downlink's service center queue id (`scQueId`), the
  result is routed by that id inside the downlink's organization, and only
  that organization's Application Center of the `acEui` is sent it.
  **Upgrade:** a downlink an Application Center queued before the upgrade and
  still in flight reports its result on MQTT and in the events only, since
  its recorded `dlDataQue` carries no service center queue id.
- A SCACI or BSSCI peer certificate is resolved to its tenant from the
  organization directory on every connection. It was read from the request
  cache, so a peer of an organization moved to another tenant or deleted kept
  its old tenant for up to the cache TTL. Per-request organization lookups
  keep the cache.
- The failure event of an attach or detach propagation that finds no
  connected base station names the endpoint, and is now recorded for the
  tenant that owns it. It was recorded for the server's default tenant, which
  was shown other tenants' endpoint EUIs; an endpoint no tenant owns is now
  only logged.
- An internal gRPC hop other hosts can reach requires the internal peer
  secret (`internal_auth.peer_secret`, `KILOCENTER_INTERNAL_AUTH_PEER_SECRET`,
  at least 32 characters). KC-Core and KC-Identity trusted the gateway's
  identity headers from any peer when the secret was empty, the default, so
  anything that reached port 50051 or 50052 could act as any tenant and user.
  KC-Core now refuses to start with `grpc.internal_trust_enabled` on a
  non-loopback `grpc.host` (an empty host binds every interface) or a
  non-loopback `identity.address` without the secret; KC-Identity with a
  non-loopback `grpc.host`; KC-Gateway with a non-loopback upstream.
  **Upgrade:** Docker Compose now requires `KILOCENTER_INTERNAL_PEER_SECRET`
  in `.env` and the Helm chart requires `secrets.internalPeerSecret`
  (`openssl rand -hex 32`, the same value for all three services); a
  single-host install on `localhost` needs nothing.

### Fixed
- **A downlink a base station holds when its deadline passes is no longer reported expired before
  the station answers.** Only the station knows whether it transmitted it, so the downlink now
  becomes **Revoking** (migration 000190): KiloCenter asks the station to drop it and reports
  `expired` only once the station confirms, says it does not hold it, or reconnects with a new
  session that discarded it. A `sent` the station reports first is recorded and reported as
  `sent`. A station that is offline at the deadline is asked again when it reconnects, also
  after a KC-Core restart, and a reconnect never returns such a downlink to the queue. A
  connected station that has not settled the downlink is asked again once per
  `protocol.downlink_expiry.sweep_interval`, never while its previous revoke awaits an answer.
  A `sent` the station that held a downlink reports after it was reported `expired` keeps the
  expiry, is logged as a warning and is recorded once, as a `dl_data_sent_after_expiry` event
  (migration 000191), so filters on `dl_data_sent` do not count it.
- **Deleting a base station ends every downlink it held as `expired`.** Its live session is
  closed, and the downlinks it held queued, reserved or **Revoking** end `expired` and are
  reported; none returns to the queue, where the endpoint could receive it twice. A deleted
  station that is still powered may still transmit them, so power it off or let its queue empty
  first. The work no longer depends on the request that deleted the station, and the expiry
  sweep ends anything a deletion left held by a station that no longer exists.
- **An MQTT command with a `ref` is never refused because of a transient failure.** When the
  organization of such a command cannot be looked up, KiloCenter publishes nothing instead of
  `mqtt.command.org_unresolved`, which now means only an organization that does not exist; a
  repeat whose `ref` is readable is recognized even when another field has the wrong type.
- **Values a client or a base station chose are logged quoted and bounded.** An MQTT `ref` or
  `expiresAt` and a base station's error message can no longer break a log line.
- **Closing the session of a deleted base station no longer logs errors** for its session record,
  which the deletion already removed.
- **Two receptions of one MQTT command that race each other no longer refuse it as expired.**
  The receptions of a command with a `ref` are queued one after the other, so the later one
  sees the first and is not answered. A deadline less than a microsecond away is refused with
  `mqtt.command.expired` instead of an internal error.
- **A base station's refusal of a revoke counts as "not held" only for the codes you name.** The
  BSSCI specification names no error code for it, so `protocol.downlink_expiry.revoke_not_held_codes`
  (default `[2]`, ENOENT; Helm `revokeNotHeldCodes`) lists them. Any other refusal, such as an
  unsupported operation or an I/O error, leaves the downlink in flight instead of ending it as
  revoked.
- **The Helm chart and `docker-compose.prod.yml` pull the published images.** Their defaults named
  `ghcr.io/kiloiot/kiloservicecenter/<image>`, where no release publishes; they now name
  `ghcr.io/kiloiot/kc-core`, `kc-gateway`, `kc-identity` and `kc-web`, including the certificate
  generator and the `rekey` upgrade hook. An installation that overrides the repository keeps its
  override.
- Creating or updating an endpoint with an all-zero network session key is
  refused with `INVALID_ARGUMENT` (`KC-GRPC-ERR-282`). Such a key can never be
  sent to a base station, and the endpoint used to be stored and then fail
  to attach with an internal error.
- Deleting an endpoint no longer logs an error when the endpoint is removed
  before its detach is recorded for the base stations.
- Certificates from `certgen` (the CA, the service center and every base
  station) no longer claim a country, province and locality of San Francisco,
  US; they name the product and the holder only.
- A request the service center refuses (invalid input, not found, not
  permitted) and a remote peer that fails the TLS handshake or drops the link
  are logged as warnings with the peer's address, not as errors, so errors
  point at faults of the service center itself.
- Downloading a base station's private key no longer hangs until the request
  times out: the key's row lock blocked the audit record of its own download.
- An uplink whose receptions by several base stations arrive at the same time
  records the endpoint's acknowledgement of its downlink once; two receptions
  could both record it, and the event appeared twice.
- MQTT `event/downlink_result` with `"result": "acknowledged"` reaches your
  broker for an uplink relayed from a federated Community Edition, and after
  an MQTT broker outage. The acknowledgement was published only by the
  process that stored the uplink, so a relayed one was recorded as an event
  and never published, and one recorded while the broker was unreachable was
  lost. It is now queued in the delivery outbox together with its record and
  published by the service center like an uplink, retried until the broker
  accepts it. Migration 000188 adds the outbox channel `mqtt_downlink_ack`.
- `CreateOrganization` stores the organization's `tags` and `description`;
  both were dropped, so an organization created in KC-Web or through the API
  read back without them. `GetOrganization` and `ListOrganizations` return
  the tags. Tags are fixed at creation: an `UpdateOrganization` that sets
  `tags` is refused with `INVALID_ARGUMENT` (`KC-GRPC-ERR-283`), where it was
  silently ignored.
- A stored base station private key that cannot be decrypted, for example
  under a wrong master key, is answered with an internal error instead of
  "certificate not found", which read as a key already downloaded. The key
  stays stored and can be downloaded once the master key is correct.
- A base station location outside the globe (latitude beyond ±90, longitude
  beyond ±180) is refused with `INVALID_ARGUMENT` and a message naming the
  valid range, instead of an internal error.
- A retried SCACI uplink delivery no longer reaches an Application Center a
  second time. When one Application Center session did not get an uplink, the
  delivery worker retried it for all of them, and every attempt sent it again
  under a new operation id with `duplicate: false`, so a session that had it
  already processed the telegram twice. An Application Center session now
  holds one `ulData` operation per stored uplink: a retry reaches only the
  sessions that lack it, a held session counts it once toward
  `protocol.scaci_resume_max_pending_operations`, and a session whose
  connection fails while the uplink is written keeps it for its resume, which
  reissues it under its original operation id (SCACI §1, §3.2, §3.8.1).
  Migration 000186 adds the unique index that keeps one such record per
  session and uplink; the upgrade preflight counts the rows its build scans.
- One uplink received by several base stations reaches the Application
  Centers and MQTT as one `ulData` listing every receiving station. The
  delivery was due the moment the first reception was stored, so a second
  station's report that arrived after the next delivery poll was stored with
  the message but never delivered. A new uplink is now delivered once the new
  `protocol.delivery.reception_window` has passed (default 500ms; Helm
  `kcCore.config.protocol.delivery.receptionWindow`), which must be shorter
  than `protocol.duplicate_window`.
- The live uplink and event streams no longer drop a row stored after a newer
  one has streamed but carrying an older time (a skewed station clock, a
  relayed or replayed uplink, a refused SCACI connect moved forward), nor rows
  sharing a timestamp. They read in the order the database stored the rows,
  reaching back by the new `grpc.stream_overlap` (default 10s). Migration
  000185 indexes the ulData rows of `messages` by storage time and adds
  `system_events.stored_at`; the upgrade preflight counts the rows the index
  builds cover.
- An uplink a base station received as a secondary receiver opens from that
  station's message list with its own RSSI and SNR instead of answering
  `NOT_FOUND`.
- Event listings include the events at both bounds of their time window.
- A new base station is no longer handed a Service Center URL it cannot
  reach. When `protocol.bsci_external_url` (or, without it,
  `protocol.bsci_host`) names a wildcard or loopback address such as
  `tls://localhost:5000`, the certificate bundle and the station carry no
  URL, and the Add Base Station result and certificate views warn the
  operator to set `protocol.bsci_external_url`
  (`KILOCENTER_PROTOCOL_BSCI_EXTERNAL_URL`).
- Renewing the server certificate keeps the names base stations connect by:
  the host of `protocol.bsci_external_url`, else the current certificate's
  subject, plus the current certificate's names and
  `certificates.server_names`. Before, renewal without an external URL named
  the certificate `localhost`, and certgen added every address of the host's
  interfaces, publishing the host's internal network layout.
- The BSSCI and SCACI listeners serve a renewed server certificate from the
  next connection on; a renewal no longer needs a KC-Core restart.
- A regenerated base station certificate is offered for download in the edit
  dialog, as the Add Base Station wizard offers a new one, and its event shows
  on the station's activity page.
- Activity date filters keep the date the admin typed, accept digits without
  separators and cover whole local days.
- A base station whose certificate was issued outside the service center is
  warned before regeneration that it is refused until it has the new files.
- A dropped gRPC-web stream behind the development proxy now ends in the
  browser too, so it reconnects instead of holding a connection.
- A downlink whose revoke the holding base station answers with an error,
  such as the AVA station's code 95 "no matching DL data found" for a
  downlink it no longer holds, now ends `revoked` instead of staying `queued`
  forever: the station will never transmit it. The revocation is recorded as
  a confirmed one is. A downlink that already ended keeps its outcome, so one
  the expiry sweep expired stays `expired` and is not reported again.
- A base station that resumes its session keeps the downlinks it had queued:
  it is sent attPrp only for the endpoints attached after its connection was
  lost, since an attPrp for an endpoint it holds makes it discard that
  endpoint's queued downlinks. A KC-Core restart or deploy no longer strands
  them until they expire. Every attach decision, including an over-the-air
  attach of an endpoint that was already attached, now records its time, so
  migration 000170 renames `endpoints.status_changed_at` to
  `attachment_changed_at` and corrects its comment; the values carry over. A
  new session, or a resumed one whose disconnect time is unknown, is still
  sent every attached endpoint.
- A downlink a base station held for an endpoint is no longer stranded as
  `queued` when the station is sent an attPrp for that endpoint (an API
  attach of an attached endpoint, an over-the-air attach heard by another
  station, or reconnection): once the station answers the attPrp, the
  downlinks it had queued for the endpoint return to pending, with no result
  reported, and go out again at the endpoint's next downlink window. A
  downlink sent to the station after the attPrp stays queued there.
- A downlink queued while the base station serving its endpoint was offline
  is sent to that station as soon as it completes its connect, new session or
  resumed, after the attachments it is sent on connect, instead of waiting
  for the endpoint's next uplink to open a downlink window. Only the downlinks
  the station serves when it connects are sent this way; a window an uplink
  opens afterwards is filled by the in-window dispatch, which still carries
  at most one downlink.
- Uplinks are stored over the packet counter's full 32-bit range (BSSCI
  §3.10.1); a counter from 2^31 on failed to persist. Migration 000167 widens
  `messages.packet_cnt` and `messages_archive.packet_cnt` to BIGINT, which
  rewrites both tables during the upgrade.
- An Application Center that reconnects after losing its connection can open
  a new SCACI session again (the earlier session no longer blocks it).
- Uplinks with empty `userData` are delivered over SCACI.
- A response to no outstanding service center operation is answered with a
  SCACI error instead of a completion.
- An uplink reissued after a SCACI resume carries its full reception details
  (`rxDuration`, `eqSnr`, `dlRxSnr`, `dlRxRssi`, `profile`, `mode`,
  `subpackets`) and its `duplicate` flag.
- A slow resume write of a SCACI connection that a newer connection of the
  same session already replaced can no longer land after that connection's
  loss and leave the stored session active without a connection: a session's
  resume and connection-loss writes follow the order of the moves.
- A resumed SCACI session whose connection drops right after `conCmp` is
  recorded as disconnected instead of active, and a resume never revives a
  session that is no longer resumable. A second `conCmp` is answered with an
  error.
- Organization look-ups report "not found" instead of an internal error, and a
  failed repository transaction always releases its connection.
- Down migrations 000145, 000154 and 000155 restore the exact previous schema.
- Downlinks without a payload (pure acknowledgements) can be queued.
- An uplink with `dlAck` relayed by federation acknowledges the endpoint's
  downlink of the previous window, as a base station's own ulData does.
- An Application Center may queue a downlink under the queue id 0, which
  SCACI allows; it was refused with `scaci.error.que_id_zero`, and its result
  could not be sent. Migration 000169 lets `downlink_queue.ac_que_id` hold 0.
- dlDataQue `userData` in the SCACI Numeric[m][n] shape is accepted.
- A counter-dependent downlink keeps its user data when it is listed,
  dispatched to a base station or reported; its stored entries were dropped
  on every read.
- Revoking a downlink the base station holds no longer fails with a database
  constraint error.
- Certificate bundle downloads are limited to the caller's tenant and no longer
  contain the CA private key; expired bundles are removed.
- The Certificates page reports the CA's subject, issuer and days until expiry.
- A normal shutdown no longer logs `gRPC server failed`.
- A fractional uplink `eqSnr` (for example 19.8 dB) is stored and forwarded to
  Application Centers instead of being dropped.
- Subpacket frequencies that base stations report with fractions of a hertz are
  stored for uplinks, attaches and detaches instead of being dropped.
- An MQTT downlink command the service center refuses (invalid, oversized, or for
  an unknown endpoint) is reported on `event/downlink_rejected` instead of being
  dropped silently.
- A SCACI `reg` of an endpoint the service center does not know creates it, as
  class A when `bidi` is set and class Z otherwise, instead of failing with
  `failed_create_endpoint`.
- A SCACI `regCmp` completes only a `reg` the service center answered with
  `regRsp`. One for a `reg` answered with an error, an already completed
  `reg`, another operation or an unknown `opId` is answered with
  `scaci.error.unexpected_register_complete` (EPROTO) and attaches nothing.
- A SCACI `regCmp` with `preAttach` attaches and propagates the registered
  endpoint. The operation log read real EUIs (above 2^53) back rounded, so the
  endpoint was not found.
- The first over-the-air attach of an endpoint is accepted with any attach
  counter, 0 included; only later attaches must advance the counter.
- An attach or detach that does not report `eqSnr` leaves the endpoint's
  recorded eqSnr unchanged instead of recording the reported `snr` as eqSnr.
- The same detach reported by two base stations is announced (`epStat`, MQTT)
  and propagated once.
- An attach or detach completed after a BSSCI session resume forwards the
  endpoint's nonce and signature in `epStat`.
- `ListDownlinkQueue` reports the base station that holds a queued downlink in
  `bs_eui` instead of `0000000000000000`, and both it and `GetDownlinkResults`
  leave `bs_eui` empty when no station holds the downlink.
- `start-dev.sh` stops the services a previous run started before starting
  new ones; its patterns matched none of them and it never stopped
  KC-Identity. The new `dev-services.sh` starts each service with a pid file
  holding the service's own PID and `dev-services.sh stop` stops exactly
  those processes and what they started (the vite server under `bun run
  dev`), KC-Web and KC-Gateway before KC-Identity and KC-Core, waiting for
  each to exit. `stop-all-services.sh` uses it and no longer kills every
  process matching `bun.*dev`.

## [1.3.0] - 2026-07-26

> **Before upgrading, run the database migrations.** Device and base-station
> EUIs are now stored in a format that covers the complete EUI-64 range.
> Existing EUIs are converted by the migration; no manual work is needed.

### Added

- **Base stations using BSSCI 1.1 can now connect.** When a station requests a
  newer protocol version than the service center supports, the service center
  offers its own version and the station accepts or declines it. Previously such
  a station was rejected outright and could not be used at all.
- **The full EUI-64 range is supported**, including EUIs that begin with a high
  byte such as `CA:FE:…`. Devices and base stations with those identifiers were
  previously rejected or stored incorrectly.
- **Device blueprints are versioned per device.** Each device keeps a snapshot of
  the blueprint it was provisioned with, so editing a blueprint in the catalog no
  longer changes how already-deployed devices decode their payloads. The catalog
  now separates built-in (System) blueprints from your own (Custom) ones,
  blueprints can be authored directly in the web interface, and many devices can
  be moved to a new blueprint version in a single operation.
- **Base-station availability and message-volume history**, so uptime and traffic
  can be reviewed over time instead of only as a current value.
- **Base stations can be identified by the EUI in their client certificate**, in
  addition to organization-based certificates.

### Changed

- Each organization now has its own tenant, and an API key grants access only to
  the organization it was issued for.
- Downlink delivery is more dependable: a queued downlink is claimed by exactly
  one delivery attempt, confirmations are safe to repeat, and a message is no
  longer lost or sent twice when a connection drops mid-transfer.
- A base station that reconnects after a network interruption resumes its
  previous session more safely. Downlinks and operations queued before the
  interruption are preserved and retried rather than silently dropped.
- Event lists and dashboards are noticeably faster on installations with large
  message volumes, and the web interface refreshes less aggressively.

### Fixed

- The web interface no longer reports "upstream circuit breaker is open" and
  stops loading data after a live page, such as a device or base-station detail
  view, has been left open for a while.
- Device attach and detach events now appear in the activity feed and are
  published to MQTT.
- Uplinks are attributed to the correct organization when a device is heard by a
  base station belonging to another organization.
- Creating a base station with an EUI that already exists reports a clear
  "already exists" message instead of a generic internal error.
- Data belonging to one organization can no longer surface in another, including
  while an organization is being removed.
- Acknowledgement-only downlinks, which carry no payload, are now accepted by
  Fraunhofer AVA base stations; they were previously rejected with error 22.
- Base-station and device pages no longer fail to load for certain EUIs.

### Security

- Certificates can only be issued for base stations in your own organization.
  Issuance now verifies ownership before a certificate is produced.

## [1.2.0] - 2026-06-15

### Added

- Decoded payload values are included in the MQTT uplink message, so
  integrations no longer have to decode payloads themselves.

### Fixed

- Downlinks are delivered reliably, and the downlink tab updates in real time.

## [1.1.2] - 2026-05-12

### Fixed

- Consistency and stability improvements across the device and base-station
  screens.

## [1.1.1] - 2026-05-12

### Fixed

- Signing in with invalid credentials no longer leaves the login button spinning
  indefinitely.

## [1.1.0] - 2026-05-12

### Changed

- The device, base-station and user screens were reorganized for faster loading
  and more consistent behavior.

### Fixed

- A fresh installation reliably creates the default administrator account.
- Several interface errors on the device and base-station screens.

## [1.0.0] - 2026-05-11

Initial public release of KiloCenter, the MIOTY Service Center: base-station and
device management, payload blueprints, downlinks, and MQTT integration.

### Added

- Container images are built and published automatically for every release.

[2.0.0]: https://github.com/Kiloiot/kilo-service-center/compare/v1.3.0...v2.0.0
[1.3.0]: https://github.com/Kiloiot/kilo-service-center/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/Kiloiot/kilo-service-center/compare/v1.1.2...v1.2.0
[1.1.2]: https://github.com/Kiloiot/kilo-service-center/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/Kiloiot/kilo-service-center/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/Kiloiot/kilo-service-center/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/Kiloiot/kilo-service-center/releases/tag/v1.0.0