CHANGELOG.md
Rama main · réplica de lectura del repositorio original.
# 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