(public) api: POST /v1/organization/{organizationUid}/member and PUT /v1/organization/{organizationUid}/member/{memberUid} accept only the manager and user roles and reject any other role with 400, so a member can no longer be given, or give themselves, a role that organizations do not support
(internal) commands and command-handler: the stored image hashes of a content guard item can be replaced after its upload finished, so hash types added later can be backfilled without re-uploading the image
(internal) common-types: optional matchScore on ImageHashesScoreSchema, the pHash-led combination of the per-hash similarity scores that screenshot-analyzer records for IMAGE and FROZEN_CONTENT items
(public) api: content guard reference images are now also hashed with dHash and pHash so image items can be matched with the combined score
(internal) command-handler: the 2026-09-21_backfill_content_guard_item_image_hashes patch fills the missing hashes on reference images uploaded before they were computed, dry run by default
(internal) DEVICE_INFO, BATTERY and RESOLUTION_LIST device telemetry types, carrying the device identity and platform specs, the battery state and the supported resolutions previously reported only by the legacy UpdateDeviceInfo, NotifyBatteryStatus and NotifySupportedResolutions actions. BATTERY is gated by the existing batteryTelemetry entitlement
(public) api: POST /v1/company/{companyUid}/member/{memberUid}/invite and POST /v1/organization/{organizationUid}/member/{memberUid}/invite send the invite email again to a member who has not activated their account yet
(internal) user-domain-model: the 2026-09-22_deviceTest_refactor_to_crud migration backfills updatedAt from createdAt and clears null finishedAt, canceledAt and failedAt on existing deviceTest documents, dry run by default
(internal) user-domain-model: deviceTestModel is built on the shared CRUD model, so it also offers fetchByUid, fetchList, update, upsert and delete. create takes the whole entity, markFinished, markCanceled and markFailed are replaced by update, and setTestSucceeded, setTestSkipped and setTestFailed take the test suite uid and the update time instead of the whole row.
(internal) user-domain-model: deviceTest documents carry updatedAt, and finishedAt, canceledAt and failedAt are optional instead of nullable.
(internal) user-domain-model: the 2026-09-22_deviceInstalledPackage_refactor_to_crud migration backfills updatedAt from createdAt on existing deviceInstalledPackage documents, dry run by default
(internal) user-domain-model: deviceInstalledPackageModel is built on the shared CRUD model, so it also offers fetchByUid, fetchList, update, upsert and delete. create takes the whole entity instead of positional arguments, and removing an installed package is deleteByDeviceAndBuildHash(device, buildHash) instead of delete(device, buildHash).
(public) api: PUT /v1/alert/{alertUid}/snooze now rejects a snooze rule it cannot apply. Only datetime (with snoozedUntil) and update (with occurrencesDiff) are accepted — any other shape used to be stored with a 204 and then silently dropped by the next alert evaluation, leaving the alert unsnoozed with no trace. The endpoint also takes the alert uid from the path now, so repeating it in the body is no longer required
(public) A device's runner and plugin assignments now end up matching the last change made to them, even when several changes are made in quick succession.
(internal) Archiving, unarchiving, snoozing or unsnoozing an alert with tens of thousands of assigned devices no longer fails permanently and no longer leaves the alert inconsistent
(public) Device date/time telemetry history records an entry only when a device's timezone, time source or clock correctness changes, or when its reported clock jumps by more than a year, instead of on every reconnect. The current date and time shown for a device is unaffected.
(internal) An alert rule can now only be read through the API by the company that owns it; other companies get the same not-found response as for a rule that does not exist.
(internal) command-handler: the 2026-09-16_remove_data_reporter_events patch derives its orphan report from the event types generated by @signageos/events instead of a snapshot grepped from the sources, so live event collections are no longer reported as orphan candidates, and it reads the event store through the native driver connection so it runs outside a pod as well
(internal) command-handler: the events repeater no longer carries an exclusion for Device.Report.ReportFileCreated; the event type and its event-store collection are both gone, so the entry guarded nothing
(internal) user-domain-model: the deviceIdentityHash_takenAt_uid__id index on deviceScreenshot is no longer created, being redundant with deviceIdentityHash_takenAt_-1_uid__id.
(public) api: GET /v1/organization and GET /v1/organization/{organizationUid} return the full organization only to organization managers and admins. Other members get an organization summary
(internal) user-domain-model: the custom script name index is unique, so the database itself rejects a duplicate (organizationUid, name) pair instead of relying only on the API and the aggregate. It replaces the older non-unique organizationUid_name index, which is dropped once the unique one exists. Schema preparation fails on startup if a database still holds duplicates — resolve them with the 2026-09-01_deduplicate_custom_script_names patch first
(internal) command-handler: closeReportingAggregate and deviceReportFileCleanAggregate — the data-reporter service is decommissioned; the Reporting.CloseDataReporter and Device.Report.CleanDeviceReportFiles commands are no longer handled
(internal) platform-consumer: Device.Report.ReportFileCreated / Device.Report.DeviceReportFilesCleaned projections into deviceReportFile
(internal) user-domain-model: deviceReportFileModel and the deviceReportFile collection wiring
(internal) command-handler: event-store patch 2026-09-16_remove_data_reporter_events drops the six removed event types' ES collections (dry-run by default) and reports orphaned events_* collections whose types no longer exist in @signageos/events
(public) api: the filesystem organization feature entitlement is now part of the OpenAPI schema, so organizations that have it can be read and updated through the REST API and its clients again
(public) Custom script names must be unique within an organization, and among managed scripts; creating or renaming a script to a name already in use is rejected with 409 CUSTOM_SCRIPT_NAME_ALREADY_EXISTS. Existing scripts that share a name keep working, but all except the most recently used copy of each name are renamed with a numeric suffix (setVolume, setVolume1, …).
(internal) events/user-domain-model: the SystemLog.SystemLogStored event and the Mongo systemLogModel with its schema preparer, sorting util and fixtures. System logs are written to and read from Loki; the Loki model, the shared ISystemLogModel interface and the exported Device.SystemLog.* events are unaffected.
(internal) user-domain-model: the screenshot half of deviceCounterModel (increaseScreenshotCounter, countScreenshotsInInterval, countScreenshotsSince and the deviceCounterScreenshot collection). It has had no writer since the screenshot counter consumer was removed and the collection emptied itself through its 48-hour TTL; the failed-action counter is unchanged.
(internal) types: Credit/creditScale utility (CREDIT_SCALE, toMillicredits, fromMillicredits, assertMillicredits) — single home for the credits/millicredits conversion used by all services
(internal) user-domain-model: credit-to-millicredits data migration — crash-safe/resumable step engine scaling every stored credit value ×1000 across read models AND event-store payloads, with exact-decimal precision-loss and non-finite audits, marker/guard-aware dry run and built-in post-run verification. Each scaled value keeps its pre-migration original, so the result is verifiable against the data it came from and a rollback reproduces the original credits; values changed after the migration are rounded back to whole credits and reported for reconciliation instead of being silently reverted. It also renames the device plan price to creditCostMillicredits everywhere a plan is stored, including the plan snapshots kept in event payloads
(public) Credit values are stored internally as millicredits (integer, 1 credit = 1000 millicredits) while the API keeps exposing credits as floats — affects credits/projectedCreditsUsage/totalUsedCredits in company responses, amount in credit transaction responses, credit in license responses, and creditCost in the device plans of company, organization and device plan history responses
(public) POST /v1/license accepts fractional credits (minimum 0.001, up to 3 decimal places enforced via multipleOf); the value is converted to millicredits before dispatching the create command
(internal) commands/events: credit-carrying fields (SetCreditBalanceToCompany/ChargeCreditsFromCompany amounts, CreditLicense* credit, credit transaction amount/balance, monthlyUsagePercentage) are integer millicredits, enforced with .int() schemas
(internal) types/commands/events: the device plan price is creditCostMillicredits, an integer in millicredits, instead of creditCost in credits — a price of 1.2 credits is now written as 1200 and a fractional value is rejected instead of being rounded at billing time
(internal) command-handler: credit aggregates assert integer millicredits on inputs and on event-store-derived state (fail loud on unmigrated/corrupt data instead of silently mis-charging 1000×)
(internal) types: optional bypassList host list in PROXY telemetry data
(public) api: bypassList in the PROXY telemetry response schema
(internal) events/user-domain-model/platform-consumer: device-reported proxy info carries an optional bypassList host list
(public) GET /v1/organization and GET /v1/organization/{organizationUid} return appletCount and customScriptCount — the number of applets and custom scripts in the organization. Like the other counts, the field is absent when the number is zero
(public) GET /v1/location and GET /v1/location/{locationUid} return deviceCount — the number of devices assigned to the location, absent when it is zero — and mapMarker, the marker icon chosen for the location, absent when the organization default applies
(public) api: the deprecated GET /v1/device/{deviceUid}/report and GET /v1/device/{deviceUid}/report/count endpoints are removed. Use GET /v1/device/{deviceUid}/telemetry/ONLINE_STATUS for the connected/disconnected (uptime) history, or an Event Stream exporter subscribed to Device.DeviceConnectionAdded / Device.DeviceConnectionDeleted for push delivery
(internal) api: POST /v1/bulk-operation and POST /v1/bulk-operation/preview now run only on the organizations the caller may write devices in. Organizations held with role viewer are dropped from the targets. A caller that may write devices in none of the requested organizations gets a 404, which the preview endpoint now documents
(public) Devices with more than one open connection no longer show up as disconnected while they are online. Reading a device's connections could drop a live connection from the index whenever another connection of the same device had just expired
(public) api: GET /v2/device and GET /v2/device/{deviceUid} now include the device's firmwareType, allowing clients to select compatible firmware updates. Devices without a reported firmware type return null, or omit the field when omitNulls=true
(public) Bulk operations can only be paused, resumed, stopped or archived from within the organizations they belong to, and only while every one of those organizations has the Bulk Actions feature
(internal) api: assigning a runner or a plugin to a device, updating an assignment's configuration and removing an assignment now wait for the change to reach the read model before responding (consistency: 'strong', domain platform), as the applet assignment and the other device endpoints already do. Reading an assignment straight after writing it could previously still return the previous state, and the follow-up GET/DELETE of a just-written assignment could answer 404
(internal) api: updating a device runner or plugin assignment now keeps the company-encrypted copy of an encrypted configuration field. The update forwards the metadata.encryptedConfigSet that the create already sent, so the value stays re-encryptable instead of being dropped on the first update
(public) The bulk operation API no longer documents onlyWithTimeOutOfThresholdMs as a device filter field. Selecting the devices of a bulk operation by how far their reported clock has drifted is not supported
(public) ACL endpoints authenticated with account JWTs now enforce configured request rate limits
(public) platform-consumer: Devices whose latest time-set action has no timezone (stored as an empty string) are no longer permanently flagged with incorrectTime — restores the "no timezone configured, skip the comparison" semantics that the isDeviceTimeIncorrect extraction accidentally narrowed to null only
(internal) api: Roles user and guest hold the same set of actions — everything role master holds except company administration (the generic update action, company and organization member listing, licensing, credit transactions and device plan history). Role guest was previously limited to reading the company public key and can now read and manage devices, tags, locations and policies. Both roles additionally gain creation of custom scripts, plugins, runners, emulators, applets, content guard categories and items, and event stream exporters. Role viewer is unchanged and stays read-only
(internal) api: GET /v1/company/{companyUid} and GET /v1/company return one of two response variants (anyOf in the OpenAPI schema): members with a manager role or above — and company network managers — receive the full company object as before, while members with role user, guest or viewer receive a company reference carrying only the identity fields uid, name, title and createdAt. Previously roles user and viewer received the full object including billing details, and role guest could not read the company at all
(public) WL/CN refactoring — expand phase (additive, all optional, no behavior change): companies and white label settings gain a companyNetworkUid, alongside the company-network-scoped model, commands, events (optional-field additions, no new revisions), startup indexes and recycle-bin deletes. Spans user-domain-model, commands, events, command-handler, platform-consumer and api. (ADR refactoring-whitelabel-and-company-network)
(public) commands: the legacy company↔company-network and company↔white-label-settings assign/unassign commands are deprecated — superseded by SetCompanyNetworkOfCompany and the company-network-owned white label settings model; removed in the Contract phase
(internal) user-domain-model: a device enters and leaves bulk operation processing through addInProgressDevice and removeInProgressDevice, replacing updateInProgressDevice, and the terminal outcome updaters no longer take decrementInProgress — the model keeps the count of devices being processed itself
(internal) user-domain-model: recording a device's outcome in a bulk operation reports whether the outcome was kept and, if it was not, why, so a consumer can log or count the devices whose outcome was rejected
(internal) api: access log lines of requests authenticated with a JWT now carry accountId — the ACL middleware fills only req.identity on that path, so the log resolves the account and organization from the identity when req.account / req.organization are not set
(public) A device which answers the same bulk operation command twice is counted once, so the number of processed devices can no longer exceed the number of devices in the operation
(public) The number of devices still being processed can no longer go negative, whether a device answered twice or the operation was paused or stopped while its commands were still being sent out
(public) api: PUT /v1/device/{deviceUid}/deprovision requires write_device on the device again instead of the generic update action — the ACL change shipped in 54.1.0 locked out every account whose role grants device management without organization-level write (e.g. role User), returning 404 on deprovision (CU-86cag9cww)
(internal) types: the firmware usage verdict no longer throws on stored device versions outside its declared type — legacy devices reported numeric versions, which crashed the firmware usage report and GET /firmware/usage-counts with a 500. Numeric versions are compared after coercion, other malformed values block as the version-not-normalizable verdict, and a missing version reads as never-reported
(public) types: firmwareVersionReferencedBy in Firmware/firmwareVersionUsage — fail-safe verdict (referenced / unreferenced / unknown + reason) deciding whether a version-string reference points at a firmware version record, using the platform's numericalVersionsEqual comparison; undecidable references are surfaced instead of guessed
(public) user-domain-model: createFirmwareVersionUsageModel — firmware version usage report across installed devices, unresolved SET_FIRMWARE_VERSION actions, policies and pending bulk operations, with per-class counts, a per-reason unknown breakdown and sample uids; safeToDelete only when nothing is referenced and nothing is undecidable. The factory requires the bulk-operation connection explicitly and scans the action log in bounded batches; supporting indexes added on the scanned collections
(public) commands/events/command-handler: firmware version deletion lifecycle — Firmware.RequestFirmwareVersionDeletion, Firmware.CancelFirmwareVersionDeletion and Firmware.FinalizeFirmwareVersionDeletion with a caller-attested usageSnapshot (validated for freshness and consistency; total === 0 unless forced; persisted into FirmwareVersionDeleted for audit). A pending deletion blocks publishing and device firmware assignment; a deleted record reads as missing everywhere and frees its (version, firmwareType, osVersion) identity
(public) commands/events/command-handler/platform-consumer: firmware version identity edit — Firmware.UpdateFirmwareVersionIdentity edits version/semverVersion/firmwareType/brand/osVersion on an unpublished, non-pending record behind the same usage gate; osVersion can be set or changed but never cleared, brand cannot become LG without one, uniqueness is validated against the live catalog, and the legacy (applicationType, version) resolution follows renames. The edit runs inside a leased identity transition (Firmware.StartFirmwareVersionIdentityTransition / Complete… / Cancel…) that closes the version to new references — device assignment, publishing, metadata changes and deletion requests reject while it is active — so the attested usage snapshot cannot be raced; a crashed dispatcher's transition expires on its own. Firmware.LinkSupersedingFirmwareVersion records the copy-on-write chain
(public) user-domain-model/platform-consumer: firmware version deletion projection — deletionRequestedAt/deletionRequestedBy while a deletion is pending, supersededByUid for copy-on-write edits, hard delete on FirmwareVersionDeleted; every firmware projection is replay-safe after the hard delete
(public) commands/command-handler: the firmware typed error classes and their deprecated registerErrorClass registrations are removed — aggregates reject with a plain CommandRejectedError carrying type constants from Firmware/firmwareCommands; the wire contract and API responses are unchanged
(public) Both sort directions of API listings now produce a consistent order — ascending is the exact reverse of descending
(public) user-domain-model: prepareFirmwareVersionTable also creates the version_firmwareType_osVersion_unique index (idempotent with the 2026-07-16 migration), so a fresh database gets the uniqueness guarantee; a legacy database with unresolved duplicates fails preparation loudly
(public) types: DeviceForceDeprovision value in CompanyFeatureEntitlements (CU-86cag9cww)
(public) api: New DeviceForceDeprovision company feature entitlement — a key-account-only gate for the destructive cross-tenant deprovision-by-authHash operation; POST /v1/device/deprovision returns 402 when the caller's company lacks it. Company-level because the use case spans all organizations of a company. The request body stays authHash-only; owned devices are deprovisioned via the ACL-guarded PUT /v1/device/{deviceUid}/deprovision (CU-86cag9cww)
(public) api: authHash in the POST /v1/device/deprovision body now requires a minimum length (CU-86cag9cww)
(public) api: PUT /v1/device/{deviceUid}/deprovision authorization goes through the standard checkPermission (Action.Write on the device) instead of the deprecated aclProvider.can with WriteDevice (CU-86cag9cww)
(public) api: the DeprovisionDevice command originator records the calling identity (via originatorFromIdentity) instead of the device's organization, so the action log attributes who performed the deprovision — relevant for cross-tenant force deprovisions (CU-86cag9cww)
(public) api: GET /v1/bulk-provisioning/recipe/count — count of provisioning recipes visible to the caller, using the same filters as the list endpoint
(public) api: GET /v1/bulk-provisioning/recipe now supports the companyUid and organizationUids filters (in addition to the existing recipe filters), and lastId cursor pagination
(public) user-domain-model: Loki.connect() throws a LokiConnectionError naming the host when Loki is unreachable or not ready, retrying until its timeout (60s by default)
(public) api: an unreachable Loki fails the service on startup, next to the Mongo, Redis and AMQP connections
(public) api: GET /v1/applet/{appletUid}/version supports case-insensitive version search and semantic-version ordering through search, sortKey=version and descending, returning the newest version first by default
(public) api: GET /v1/device/{deviceUid}/report and GET /v1/device/{deviceUid}/report/count (data-reporter CSV report files) are deprecated and will be removed. Use GET /v1/device/{deviceUid}/telemetry/ONLINE_STATUS telemetry history or an Event Stream exporter subscribed to Device.DeviceConnection{Added,Deleted} instead
(public) user-domain-model: invalidateAllByDevice on the latest monitoring-log models — deleting a device's stored latest telemetry is never correct, as nothing refills it but the device reporting that telemetry type again.
(public) user-domain-model: Optimize fetchUidListByOrganizationUids by adding organizationUid, uid composed index (to save in-memory sort in mongo by uid)
(public) api: GET /v1/organization applies the uids and organizationUid query filters consistently with the caller's organization permissions
(public) api: Assigning the company owner role, and changing or removing a member who holds it, requires a different company owner. Management of every other company role is unchanged
(public) Device request commands accept an optional requestUid, so a producer can record the uid that will correlate a device's result before dispatching the command
(public) Device request commands that previously had no schema (volume, brightness, application and firmware version, time, debug, remote control, resize, test suite, timer, RM server, power status and the display family) are now validated on ingress — a payload that does not match its command type is rejected with invalid_command instead of being forwarded to the device
(public) Creating an UPDATE_TIME bulk operation requires a non-null timezone; null was accepted before and reached devices as an invalid value
(public) command-handler: on-demand inject aggregates producing ApplicationInjectRequested and the terminal inject events
(public) user-domain-model: applicationInjectRequestModel read model over the applicationInjectRequest collection
(public) platform-consumer: projects ApplicationInject{Requested,Succeeded,Failed} into applicationInjectRequestModel
(public) api: GET /v1/device/configuration/telemetry-intervals and GET /v1/device/configuration/telemetry-intervals/count list the telemetry check intervals of every device in the organization, with cursor pagination
(public) user-domain-model: uid, createdAt and organizationUid on deviceConfiguration, backfilled by the 2026-08-17_deviceConfiguration_uid and 2026-08-17_deviceConfiguration_organizationUid migrations. Deploying before they run breaks nothing, but until then reads by uid miss the untouched rows and the telemetry-intervals listing has no real createdAt to order by. Each field is filled on its own and only where absent, so a re-run is a no-op and a row that already has uid still gets createdAt. Both are dry by default
(public) user-domain-model: deviceConfiguration is indexed by uid, and by organizationUid with createdAt and deviceIdentityHash, which serves the new telemetry-intervals listing. The uid index is not unique — uid is copied from the already unique deviceIdentityHash, so a unique index would add no guarantee while refusing to build until the backfill has run
(public) user-domain-model: deviceConfigurationModel extends CRUDModel, so reads go through the generic fetchOne/fetchList/count. The write methods take a deviceIdentityHash identification instead of a whole configuration row, and create/update/updateByFilter throw — the entity still carries null fields the generic writes would drop, so createLegacy/updateLegacy have to be used until it is null-free
(public) api: PUT /v1/device/configuration/:deviceUid/telemetry-intervals accepts whole milliseconds between 300000 and 2098800000, now documented in the specification. 300000 is the Ultra entitlement floor, the lowest any plan can use — the endpoint used to refuse everything below 3600000, the floor for an organization with no telemetry entitlement at all, so it rejected intervals Box lets the same organization set
(public) api: debug and firmwareVersion are documented on the telemetry interval schemas; the endpoint already accepted them but the specification never listed them
(public) api: GET/PUT /v1/device/configuration/:deviceUid/telemetry-intervals and POST /v1/device/configuration/:deviceUid/telemetry-cache/invalidate are served by the ACL router, so they accept account and JWT authentication and answer 404 instead of 403 for a device of another organization
(public) platform-consumer: a device keeps organizationUid on its configuration in sync with the device, and the configuration is created with the organization it belongs to
(public) user-domain-model: a deviceConfiguration row inserted by one of the legacy upserts gets a createdAt, which every one of them left out. It is required by CRUDEntity and is the sort key of the new telemetry-intervals listing, so a row written that way was ordered arbitrarily there
(public) Event stream exporters can subscribe to Alert.AlertArchived and Alert.AlertUnarchived — emitted when an alert is resolved globally
(public) All exported alert events (Alert.AlertDeviceAssigned, Alert.AlertDeviceUnassigned, Alert.AlertArchived, Alert.AlertUnarchived) carry alertRuleUid, so consumers can tell which alert rule fired or was resolved
(public) Alert email notifications are sent again for alerts that were unarchived — archiving an alert corrupted its stored alert rule reference, which silently disabled the rule's notifications from then on
(public) platform-consumer: Device history now records plugin and runner assignment changes
(public) user-domain-model: the 2026-08-18_telemetry_backdated_online_status_repair migration repairs telemetry left corrupted by backdated online-status recovery events across all organizations within a required SINCE time window — history ordering that skewed uptime, negative offline ranges, and stale latest values in deviceMonitoringState and Redis. It is dry by default and must run only after the device-status-checker stamping fix is deployed
(public) api: paginated GET listings only fetch one page of documents from the database instead of streaming surplus batches, making them faster
(public) user-domain-model: devices have a compound index on organizationUid, createdAt and uid, serving organization-scoped listings like GET /v1/device/policy from an index instead of a scan
(public) platform-consumer: ONLINE_STATUS telemetry is cached in arrival order instead of being dropped when its createdAt predates the cached record. A device that came back online within the offline threshold stayed cached as offline, because device-status-checker stamps an offline with lastAliveAt plus the threshold but a recovery with the ping's own time. Every other telemetry type keeps the ordering check, which protects the current value from telemetry a device buffered while offline
(public) user-domain-model: the 2026-08-14_deviceMonitoringState_onlineStatus_redis_repair migration repairs the Redis ONLINE_STATUS cache of one organization (ORGANIZATION_UID) from deviceMonitoringState, for devices left stuck as offline before the fix. It is dry by default
(public) user-domain-model: the 2026-08-05_deviceActionLogLatest_organizationUid_repair.js migration walks only the rows present when it starts, so writes during the run cannot prolong it; a re-run picks up the rows written in the meantime
(public) api: the validator generator handles anyOf request bodies with titled variants
(public) api: azure-event-hub exporter target on /v1/event-stream/exporter — SAS key values are never returned
(public) api: Device.SystemLog.* event types accepted in subscribedEventTypes for azure-event-hub exporters, rejected for webhook
(public) Event stream exporters can subscribe to Alert.AlertDeviceAssigned and Alert.AlertDeviceUnassigned — emitted once per device when an alert starts or stops being active on it, carrying the owning organization
(public) api: exporter-config access checks resolve the organization of every requested config in a single event-stream-distributor request instead of one per config
(public) user-domain-model: deviceActionLogLatest has a unique index on deviceIdentityHash and type
(public) api: GET /v1/device/<action>/latest listings are served by indexes instead of collection scans and joins, cutting response times from seconds to milliseconds
(public) api: GET /v1/device/<action>/latest pagination no longer skips or repeats rows sharing a createdAt
(public) platform-consumer: The TAKE_SCREENSHOT device action log is created only when a device reports a failed instant screenshot, already marked as failed. Requested and successful screenshots are not logged anymore, they were flooding the action log. The 2026-08-11_deviceActionLog_remove_unfailed_take_screenshot migration deletes the already logged TAKE_SCREENSHOT entries without failedAt from deviceActionLog and deviceActionLogLatest
(public) events: DeviceTakingInstantScreenshotFailed carries the deviceIdentityHash of the device that failed to take the screenshot
(public) commands: registerErrorClass and getErrorClass — aggregates no longer need typed error classes to get a 400, since every process_failed response is now mapped to one with its message. Registration was also unreliable: it is a module-load side effect with no barrel, so most types never resolved in the api and fell back to the base class anyway
(internal) api/platform-consumer tests: the pbkdf2 work factors of @signageos/lib's token/password encryption are lowered test-only (tools/test/fastEncryption.cjs
(internal) tests: the four integration test suites (api, command-handler, platform-consumer, user-domain-model) run concurrently
(public) command-handler: A defect in aggregate logic (a null dereference, an invalid date) is no longer reported as a rejected command. It is answered with an error response instead of process_failed, so the caller reports a generic server error rather than quoting the internal message back to the client, it is logged as aggregate defect, and it is counted as command_erred instead of command_failed
(public) api: a command rejected by the command handler now always returns 400 with the rejection reason instead of a generic 500 — a process_failed response means the command was rejected, i.e. a client error, and it always carries a detailed message. Only rejections that happened to arrive as a registered typed error class were mapped before, so e.g. DELETE /v1/custom-script/{customScriptUid} on a script that still has versions (without deleteVersions) returned 500 UNEXPECTED_SERVER_EXCEPTION. Applies to every command, not just custom scripts
(public) api: An organization member with the user role can again manage locations and device policies, provision devices and deprovision them from the device detail page
(internal) api: ContentGuard AI Helper can run on GCP Vertex AI (Gemini) as an alternative to AWS Bedrock — select with CONTENT_GUARD_AI_PROVIDER (bedrock, the default, or vertex) and configure via GCP_VERTEX_IMAGE_ANALYSIS_MODEL_ID, GCP_VERTEX_PROMPT_ANALYSIS_MODEL_ID and optional GCP_VERTEX_PROJECT/GCP_VERTEX_LOCATION (Application Default Credentials can supply them). AWS_BEDROCK_* env vars are required only when the bedrock provider is selected
(public) api: Package and package-version CRUD endpoints now support paginated listing, creation, update, recycle-bin restore, binary upload and download, icon retrieval, publish and unpublish, and package deletion
(public) api: Package version uploads require an explicit non-empty metadata version and return 400 Bad Request for invalid binaries; publish and unpublish transitions are idempotent
(public) api: Package deletion returns a conflict while versions or policy references exist and otherwise moves the package to the recycle bin
(public) types/user-domain-model/api: BLANK_SCREEN content guard item type — a managed, reference-image-free item scored on how uniform each screenshot is instead of by comparison with an uploaded image. It lives in an ordinary category, so a single category can mix BLANK_SCREEN with IMAGE and PROMPT items and one alert rule covers all three
(public) command-handler: BLANK_SCREEN items can only be created in managed categories
(public) commands/events: optional whitelabel branding metadata on CoreApp build requests — applicationBrandingTitle and applicationBrandingIconUrl on the BuildApplicationVersion command and application.branding (title, iconUrl) on all ApplicationVersionBuild*Requested events; command-handler maps the command fields into the produced events
(public) events: Device.Screenshot.DeviceTakingInstantScreenshotFailed and Device.Screenshot.DeviceTakingInstantScreenshotSucceeded events reporting the instant screenshot attempt result, paired to the request by the echoed uid
(public) events: uid on DeviceTakingInstantScreenshotRequested (with originator) identifying the instant screenshot request
(public) commands: uid on RequestInstantScreenshot identifying the screenshot request (schema-generated when not provided)
(public) command-handler: Pass the screenshot request uid and originator into DeviceTakingInstantScreenshotRequested
(public) platform-consumer: Track instant screenshot requests in the device action log (TAKE_SCREENSHOT) — created on request, marked failed on DeviceTakingInstantScreenshotFailed and succeeded on DeviceTakingInstantScreenshotSucceeded, all consumed by the platform consumer on the main events connection; a response consumed before the request is retried via redelivery for up to 15 minutes before being dropped
(public) types: TAKE_SCREENSHOT device action type
(public) api: Originator enrichment (createdBy/updatedBy/originator fields on list and detail endpoints) no longer issues Mongo queries with empty $in arrays, deduplicates the looked-up organization uids and account ids, and runs the organization and account lookups in parallel
(public) user-domain-model: fetchListByUids (all models) and accountModel.fetchListByIds return immediately on empty input without querying Mongo and deduplicate their input before building the $in filter; the Memory implementation of fetchListByUids no longer returns duplicate entities for duplicated input uids
(public) user-domain-model: createLockedDomainModel no longer registers process.once('SIGTERM'/'SIGINT') shutdown hooks at creation — they are registered lazily on the first waitForTimestamp() call and removed on close(). platform-consumer creates the model per consumed event batch (session-scoped), so the unconditional registration leaked 2 process listeners pinning the whole model graph (including the ended ClientSession) for every batch — the linear memory growth that OOMKilled the mongo-domain platform-consumer pods in production (~34 MB/min under load, 1 GiB limit reached in ~25 min on prod-us1)
(public) api: GET /v1/device/telemetry/latest logs one line per request with phase timings (device fetch, telemetry fetch, join) and item counts
(public) api: GET /v1/device/telemetry/latest accepts an optional repeatable types query parameter returning only the given telemetry types for each device (the full response carries all ~44 types; a single-type page is roughly 44× smaller)
(public) user-domain-model: readable models' fetchList accepts optional fields (server-side projection); the returned cursor is typed as a Pick of the requested fields, so reading a field that was not projected does not compile
(public) api: GET /v1/device/telemetry/latest built each device's telemetry list by rescanning the full telemetry array per device (O(n×m), ~44M comparisons at 1000 devices); telemetries are now grouped by device in one pass
(public) user-domain-model: the Redis latest-telemetry read (fetchLatestListByDevices) awaited its MGET batches one after another (44 sequential round trips at 1000 devices); the batches now run concurrently so the client pipelines them
(public) api: GET /v1/device/telemetry/latest decoded full device documents with no server-side limit only to read a few fields; the device query now projects the mapped fields and stops at the page size
(public) user-domain-model/commands/events: CustomScript.organizationUid is now optional — scripts without an owning organization are managed (global) signageOS scripts; a non-unique organizationUid index is added to the custom script collection
(public) command-handler: creating, updating, deleting, or writing versions/releases of a managed custom script is restricted to admin accounts (AccessLevel.Admin); organization-owned scripts are unchanged
(public) platform-consumer: managed CustomScriptCreated events (no organization) no longer touch any per-organization script count
(public) api: GET/POST /v1/custom-script/managed and GET/PUT/DELETE /v1/custom-script/managed/{customScriptUid} — read managed scripts (any organization with the scripts entitlement) and create/update/delete them (admin accounts only)
(public) api: PUT /v1/device/{deviceUid}/custom-script now also executes managed (global) scripts — any organization with the scripts entitlement can run a managed script on its own devices
(public) api: the existing custom-script version and platform endpoints (/v1/custom-script/{uid}/version...) now support managed (global) scripts — reads are available to any organization with the scripts entitlement, writes (incl. the archive upload URL) are restricted to admin accounts
(public) api: dev-only DANGEROUSLY_allow_static_token_admin env var (default off) grants admin identity to admin accounts authenticating with static account API tokens so the CLI can manage managed custom scripts on development environments — must never be enabled in production
(public) api: excludeFeatureContext query parameter on GET /v1/location — when true, omits feature.properties.context (the single largest part of the payload, ~40%) from every returned location; opt-in only for backward compatibility with consumers that may read context
(public) api: documented the real feature shape (raw persisted Mapbox geocoder result) in the location OpenAPI schema, replacing the placeholder name/amenity/popupContent keys
(public) types: DEVSPACE_PLATFORM_FENTS lists the DevSpace platform-specific company feature entitlements (one per CoreApp build target), so consumers can share one allow-list instead of re-declaring it. Sits alongside DEFAULT_COMPANY_FEATURE_ENTITLEMENTS in Company/Company.ts. Intended for server-side authorization of self-service platform selection — every company feature entitlement outside this subset is commercial and admin-only.
(public) api: ACL checks no longer issue one set of MongoDB queries per checked entity (N+1) — request-scoped providers derive an identity's privileges once and batch entity reads into $in queries. ACL decisions are unchanged except for the one below.
(public) api: An admin is no longer granted access to a credit license whose document carries no companyNetworkUid (non-admins were already denied).
(public) user-domain-model: createEventsInvalidatedCache takes any Cache instead of requiring a CacheWithWarmup, and no longer re-exposes warmup on the wrapper it returns — event invalidation is independent of warming, so a cache whose data has no enumerable source (e.g. a plain RedisCache fed only by writes) can be wrapped without faking a warmup. createEventsInvalidatedCacheFromModelReadableFactory still returns a warmable cache, so its consumers are unaffected
(public) Duplicate sos_redis_driver_errors series with identical labels (dropped by Prometheus as "different value but same timestamp") and sos_redis_driver_connection_ready/_end counters silently not counting after the first redis error
(public) api: Rate limiting is now enforced on device deprovision (POST /v1/device/deprovision, PUT /v1/device/:deviceUid/deprovision) and device verification (POST /v1/device/verification, GET /v1/device/verification/:deviceVerificationUid) endpoints to reduce brute-force attack surface
(internal) api: Request rate limits are now static configuration provided via the REQUEST_RATE_LIMITS env var (a JSON array of {method, route, windowInMs, requestLimit} rules) instead of the api-config MongoDB requestRateLimit collection; the api-config Mongo connection and its mongo_db_api_config_dsn env var have been removed. The config is validated at startup (invalid route patterns and sub-second windows fail the boot); route patterns are precompiled once; and the per-identity counter now uses an atomic redis INCR so concurrent bursts cannot exceed the limit
(public) user-domain-model: organizationUid_createdAt index on the timing, location, and location recycle-bin collections — supports org-scoped list filtering with the default createdAt sort and cursor pagination
(public) types: ExporterConfigSchema restructured into a per-target union — webhook exporters may subscribe to domain and telemetry event types only, azure-event-hub exporters to the full universe including system-log types; added getAllowedEventTypesForConfigType so consumers never reach into schema internals
(public) commands: ExporterConfigEntitySchema, CreateExporterConfigSchema and UpdateExporterConfigSchema are per-target unions rejecting invalid config/subscribedEventTypes pairings
(public) command-handler: UpdateExporterConfig rejects event types not allowed for the exporter's effective config type; ExporterConfigUpdated events now carry only the fields provided by the command
(public) commands: createUpdateEntityCommandSchema strips .default() from entity fields, so a partial UpdateExporterConfig that omits enabled no longer injects enabled: true (which re-enabled disabled exporters on rename-only updates)
(public) user-domain-model: fetchList method on ILockedDomainModel — fetches many locked domains in a single round trip (Mongo: one $in query on the domain_lockKey index; Redis: batched MGET)
(public) platform-consumer: The lockedDomain freshness check at the end of each event batch (filterNewerUpdates) now reads the current timestamps of all lockKeys in one bulk fetchList query instead of one serial findOne per lockKey inside the batch transaction — removes up to batch-size database round trips per batch under per-device event floods
(public) commands: account emails (CreateAccount, CreateSimpleAccount, AddAccountToCompany, AddAccountToOrganization) are normalized to lowercase at schema level — applies to producer factories and command-handler input validation, so case-variant invites resolve to the same account instead of creating duplicates
(public) command-handler: AddAccountToCompany and AddAccountToOrganization now reject an email that already belongs to a member of the target company/organization (self-invite included) instead of silently overwriting the member's role — re-inviting an owner as guest no longer downgrades and locks out the owner; role changes must use the privilege-update commands
(public) api: POST /v1/company/{companyUid}/member and POST /v1/organization/{organizationUid}/member now return 409 Conflict (COMPANY_MEMBER_ALREADY_EXISTS / ORGANIZATION_MEMBER_ALREADY_EXISTS) when the invited email already belongs to a member, matching case-insensitively — previously the invite silently overwrote the member's role
(public) command-handler/commands: the last-owner protection ("Company must have at least one owner") is now a typed command rejection (Account.Privilege.CompanyMustHaveOwner) that survives the command RPC — demoting or removing a company's only owner returns a clear 400 with that message from the API instead of a generic 500
(public) api: member invite endpoints (POST /v1/company/{companyUid}/member, POST /v1/organization/{organizationUid}/member) no longer wait for read-model synchronization — on environments with lagging consumers the sync wait timed out and returned 500 for invites that had already succeeded; the invite is confirmed once the command-handler accepts it
(public) api: observability.lastSuccessfulExportAt on Event Stream exporter responses is no longer documented as required — a freshly created exporter has never delivered anything, so the field may be null or absent
(public) types: incorrectTime, timestamp, and timezoneOffset fields on the DATETIME monitoring log data type (ntpServer becomes optional)
(public) platform-consumer: Compute incorrectTime (timezone mismatch against the platform-configured timezone, or clock deviation over ±10 minutes) on every device current time report and persist DATETIME monitoring logs into telemetry history, the latest snapshot, and the Redis telemetry cache serving the aggregate latest-telemetry endpoints
(public) api: incorrectTime in the device response currentTime object and in DATETIME telemetry responses (/v1/device/:uid/telemetry/latest, bulk /v1/device/telemetry/latest, and /v1/device/:uid/telemetry/DATETIME/latest)
(public) api: incorrectTime boolean filter on GET /v2/device and GET /v2/device/count (devices that never reported time match neither filter value)
(public) api: POST /v1/device/{deviceUid}/custom-script/latest returns the latest execution for each requested Custom Script in one request
(public) user-domain-model: Add an indexed grouped latest-action-log query
(public) platform-consumer: An error while processing a single cache-invalidation event no longer crashes the whole consumer process — the failed event is dead-lettered for delayed retry individually while the remaining events keep processing; cache invalidation now waits for DB synchronization before resolving the cache key (closing a race with the projection for freshly created devices); a DeviceOrganizationUpdated event for a device missing from the read model (deleted) is treated as a no-op instead of an infinitely retried error; unhandled promise rejections are logged instead of terminating the process; AMQP consumers now start only after service.start() so the process-level error handlers are registered before the first message can arrive
(public) user-domain-model: The per-device bulk operation progress writes (updateSuccessfulDevice, updateFailedDevice, updateSkippedDevice and the increment branch of updateInProgressDevice) now append with $push instead of $addToSet. The stored result is identical thanks to the existing { $ne } filter guards, and the full-array rewrite that capped bulk operations at ~10 devices/s regardless of the configured concurrency is gone.
(public) api: Document Device Custom Script execution pagination and accept canonical result pipeline values without removing legacy values
(public) user-domain-model: The unused deviceTemperature model (createDeviceTemperatureModel, prepareDeviceTemperatureTable, the IDeviceTemperature* schema interfaces and the DeviceCollection.Temperature entry). Nothing ever read from or wrote to the deviceTemperature collection — device temperatures are recorded and served through the device monitoring log (DeviceTelemetryType.TEMPERATURE). The existing collection is left untouched in MongoDB and expires on its own TTL index.
(public) api: The deviceTemperatureModel instance in basicModels, which was constructed but never used. GET /v1/device/:deviceUid/temperature is unaffected — it already reads from the device monitoring log.
(public) platform-consumer: The deviceTemperatureModel instance in mongoModels and its redisModels/models entries, which were constructed but never used.
(public) types: computeDefaultedFeatureFlags helper and ENTITLEMENT_DEFAULTED_DEVICE_FEATURE_FLAGS map in Device/FeatureFlag — defaults unset cpuUsage/memoryUsage device feature flags to enabled when the organization holds the cpuTelemetry/ramTelemetry feature entitlements; explicitly stored values are never overridden
(public) API: The per-device plugin, runner and screenshot write endpoints (POST/PUT/DELETE /v1/device/:deviceUid/plugin, .../runner, and POST /v1/device/:deviceUid/screenshot) now verify the Plugins/Runners/Screenshots entitlement against the organization that owns the target device (via checkPermission's fent), regardless of the caller identity. This closes the gap where a multi-organization account entitled in one organization could act on a device owned by a non-entitled organization. The matching read endpoints keep the caller-organization gate.
(public) API: The organization entitlement cache is kept hot so the caller-organization gate avoids cold read-throughs: it is warmed on startup (after a jittered delay) and re-warmed on a jittered ~4h schedule (all organizations streamed via one fetchList, in the background/non-blocking), and an entitlement change reloads the affected organization's entry instead of only evicting it. It has no TTL — freshness comes from that active invalidation-reload plus the periodic re-warm, not from expiry. Best-effort: the batched read-through remains the fallback, so warmup/reload failures degrade to lazy fetches rather than errors.
(public) user-domain-model: model-readable caches implement CacheWithWarmup — a warmup() that streams the full set (one fetchList) into the cache — and accept ttl: null to never expire; EventsInvalidatedCache gains an opt-in reloadOnInvalidate that re-fetches an invalidated key (keeping it hot) instead of only evicting it.
(public) API: The caller-organization feature-entitlement gate no longer fans out into one entitlement lookup per organization for account tokens — EntitlementProvider.canAny resolves all of the account's organizations in a single batched read, removing an N+1 that raised latency on device endpoints for accounts spanning many organizations.
(public) user-domain-model: Cache.getMany now resolves read-through misses with a single batched $in query (one fetchList) on model-readable caches — for any key, not just uid — instead of one fetchOne per missing key.
(public) api: GET /v1/organization/:organizationUid/device-plan-history endpoint returning an organization's device plan change history (newest record first, human-readable originator), restricted to owner and master roles via the new read_device_plan_history ACL action
(public) command-handler: Record every device plan assignment into the append-only devicePlanHistory on organization create and update ({ plan, originator, date } where plan is a snapshot of the whole assigned device plan as the source of truth for auditing; recorded even when the same plan is set again), starting with the initial plan assigned at organization creation
(public) platform-consumer: Project devicePlanHistory into the organization read model
(public) user-domain-model: Add append-only devicePlanHistory to the organization model
(public) types: Add device plan change history type to Company
(public) api: POST /v1/applet/{appletUid}/version and PUT /v1/applet/{appletUid}/version/{appletVersion} now accept frontAppletVersion: null (bundled front applet) in the JSON body instead of rejecting it with 400 INVALID_BODY_PROPERTIES — the write path already handled null (bundledFrontApplet), only the request schema was too strict
(public) command-handler: Allow creating applicationVersion without frontDisplayVersion
(public) platform-consumer: Allow creating applicationVersion without frontDisplayVersion
(public) api: GET /v1/device/telemetry/latest/count now supports filtering devices by their latest online status using type=ONLINE_STATUS together with the online query parameter
(public) api: PUT /v2/device/{deviceUid}/firmware — device firmware upgrade addressed by the firmware version UID, unambiguous even when the same manufacturer version exists for several firmware types or OS versions; gated by the FirmwareUpgrades entitlement like V1; the V1 version-addressed endpoint is deprecated but stays operational
(public) api: GET /v2/firmware (list, count, by-uid) now returns firmwareType, brand, osVersion, and a computed prettifiedName (webOS manufacturer versions displayed in normalized numerical form, raw values preserved), and supports firmwareType[Prefix|Suffix], brand, and osVersion filters; search now matches firmwareType and brand instead of the deprecated deviceTypes
(public) command-handler: LG-only firmware/device compatibility validation for UID-addressed upgrades — the firmware type and OS version must equal what the device reported (fails closed when unreported) and the normalized manufacturer version must differ from the one already installed; rejected with the Firmware.IncompatibleWithDevice error; non-LG behavior preserved
(public) commands/events: Firmware.UpdateFirmwareVersionMetadata command and Firmware.FirmwareVersionMetadataUpdated event replace the device-types update (Firmware.UpdateFirmwareVersionDeviceTypes removed; the old event stays replayable); FirmwareVersionCreated revision 3 carries scalar firmwareType plus optional brand/osVersion with an upcaster from revision 2
(public) common-types: normalizeNumericalVersion/numericalVersionsEqual helpers for numerical manufacturer/OS version comparison (03.30.16 ≡ 3.30.16)
(public) user-domain-model: firmware version compatibility is now the scalar firmwareType (with optional brand and osVersion, missing on legacy records) instead of the deviceTypes list, which is kept in sync as a deprecated single-item array; firmware version uniqueness moved from (applicationType, version) to the (version, firmwareType, osVersion) tuple, validated authoritatively by command-handler with normalized version semantics (6.1-UL5Q-03.15.60 == 03.15.60 == 3.15.60) — the unique partial index on the raw fields is an exact-format backstop. The event store is the single source of truth for the split: the required command-handler patch 2026-07-16_firmware_version_split_by_firmware_type splits multi-type created events (original UID stays with the first listed type, siblings get deterministic sha256-derived UIDs; records merged by the 2026-06-12 read-model consolidation are recreated under their original event UIDs) and reconciles the platform firmwareVersion collection from the patched events in the same process; the 2026-07-16_firmware_version_firmware_type migration then only backfills scalar fields, resolves duplicates, and creates the unique partial index — it aborts if any unsplit multi-type document remains
(public) command-handler: firmware command rejections now retain their structured error type, preventing expected validation failures from returning HTTP 500
(public) platform-consumer: Device history now records organization tag and location assignment changes
(public) platform-consumer: Device export events (DeviceListExportRequested, DeviceListExportSucceeded, DeviceListExportFailed) are now consumed in batches; export data requests are created and updated with bulk database operations
(public) Exclude Windows devices from auto-banning on serial number change in UpdateDeviceInfo
(public) command-handler: UpdateDeviceInfo now completely ignores a serial number change from an already known value on Windows devices — no DeviceManufacturerDetailsUpdated is emitted for the serial number and the device is not banned (other fields still update normally)
(public) api: POST /v1/applet/{appletUid}/version/{appletVersion}/publish, .../deprecate, and .../renew — set an Applet Version's lifecycle status; guarded so publishing requires a successfully built, not-yet-published version, deprecating requires a non-deprecated version, and renewing requires a deprecated version (409 Conflict on invalid transitions)
(public) api: status field (draft | published | deprecated, derived from publishedSince/deprecatedSince) on the Applet Version resource
(public) api: Content Guard and Policy/Tag operation tags were missing from the global OpenAPI tags declaration, causing documentation tooling to append them out of alphabetical order; the global tags list is now inlined in openapi/schema.yml, sorted alphabetically, and enforced by the operation-tag-defined and tags-alphabetical Redocly lint rules plus a new tags consistency test suite; removed the orphaned deviceOfflineRange.yml spec file that was no longer referenced by any path
(public) api: POST /v1/package/:packageUid/version now allows recreating a soft-deleted package version — creation is only rejected with PACKAGE_VERSION_EXISTS_ERROR when a version with the same buildHash (or metadata version) still exists and has not been deleted
(public) api: Applet version endpoints (/v1/applet/:appletUid/version...), device action log list (/v1/device/:deviceUid/action-log), and device applet command list/create (/v1/device/:deviceUid/applet/:appletUid/command) returned 400 INVALID_PATH_PARAMS for URLs with a trailing slash — trailing slash is now normalized before OpenAPI template matching, restoring pre-migration behavior
(public) api: PUT /v1/organization-tag/:uid request body now accepts parentTagUid: null to detach a tag from its parent (previously a string, or omitted to leave it unchanged)
(public) command-handler: CreateTiming now rejects with Cannot create timing for deprovisioned device when the target device does not belong to an organization (deprovisioning removes it). Closes a race where device-policy-manager re-created a policy timing milliseconds after deprovision (reacting to the deprovision's own TimingDeleted, whose originator has no policyUid), leaving an orphaned timing on a deprovisioned device
(public) api: POST /v1/timing facade now rejects creating a timing for a device that does not belong to an organization (deprovisioned or never provisioned) with 400 DEVICE_NOT_PROVISIONED_TO_TIMING_CREATE, instead of dispatching a CreateTiming that would create an orphaned timing on an unowned device
(public) Device remote-control (kiosk mode / IR remote-control lock, PUT/GET /v1/device/:deviceUid/remote-control) now requires OrganizationEntitlements.DeviceManagement instead of RemoteDesktop. In 44.0.0 it was gated behind the premium Remote Desktop add-on, which returned 402 (INSUFFICIENT_REMOTE_DESKTOP_ENTITLEMENT) for organizations that lock devices into kiosk mode (e.g. via sos kiosk mode) without that add-on. Kiosk locking is core device management and is now consistent with the sibling device-security endpoint.
(public) Device policy status endpoints (list, get by item type) require OrganizationEntitlements.DevicePolicy
(public) API: Feature Entitlement (OrganizationEntitlements.ContentGuard, OrganizationEntitlements.Tags, OrganizationEntitlements.AIContentGuard) enforcement on all Content Guard endpoints — requests from organizations without the required entitlement now receive HTTP 402
(public) Location endpoints (GET/POST/v1/location, GET/PUT/DELETE/v1/location/{locationUid}, add-attachment, remove-attachments, archive, unarchive, restore) now require the Locations feature entitlement on the caller's organization (402 otherwise)
(public) Location organization-tag list endpoints (GET /v1/location/organization-tags, GET /v1/location/{locationUid}/organization-tag) require the Locations entitlement; assign/unassign (PUT/DELETE /v1/location/{locationUid}/organization-tag/{tagUid}) require Locations (caller organization) plus Tags (the tagged location's organization)
(public) Plugin endpoints — GET/POST/PUT/DELETE /v1/plugin/:pluginUid and all Version and Version-Platform sub-resources — now require the OrganizationEntitlements.Plugins organization entitlement and return 402 without it; GET /v1/plugin (list) filters results to entitled organizations (402 only when the caller has no entitled organization)
(public) Runner endpoints — GET/POST/PUT/DELETE /v1/runner/:runnerUid and all Version and Version-Platform sub-resources — now require the OrganizationEntitlements.Runners organization entitlement and return 402 without it; GET /v1/runner (list) filters results to entitled organizations (402 only when the caller has no entitled organization)
(public) Bulk operation endpoints require the BulkActions feature entitlement on the caller's organization (402 otherwise)
(public) Bulk provisioning recipe endpoints require the BulkProvisioning feature entitlement on the caller's organization (402 otherwise)
(public) Device tag endpoints (GET /v1/device/tag, GET/POST/DELETE /v1/device/{deviceUid}/tag[/{tagUid}]) now require the Tags feature entitlement on the caller's organization (402 otherwise)
(public) Device plugin set (PUT /v1/device/{deviceUid}/set-plugin/{pluginUid}) now requires Plugins, and device runner set (PUT /v1/device/{deviceUid}/set-runner/{runnerUid}) requires Runners, on the caller's organization (402 otherwise)
(public) Device location assign/unassign (PUT/DELETE /v1/device/{deviceUid}/location/{locationUid}) now require the Locations feature entitlement on the caller's organization (402 otherwise)
(public) Per-type latest device telemetry (GET /v1/device/{deviceUid}/telemetry/{telemetryType}/latest) now requires the telemetry type's entitlement on the device's organization (402 otherwise)
(public) events: the event documentation generator now expands constrained generic event payloads to their real shapes instead of an empty {} — e.g. DeviceTelemetryRecordUpdated now documents name as the DeviceTelemetryType enum and data as the full MonitoringLogData payload union (with nested field descriptions preserved)
(public) events: the event documentation generator now preserves field-level JSDoc through the type-fest identity wrappers Simplify/ReadonlyDeep/WritableDeep, so entity create/update events (ICreateEntityEvent/IUpdateEntityEvent) document their fields instead of dropping the descriptions during mapped-type reconstruction
(public) events: documented every field of the public events (device connection, provisioning recipe and telemetry events); the shared type, uid and originator fields are described once on their base types (IEvent, IEntity, IOriginatorAwareEvent)
(public) events: the event documentation generator now surfaces zod .describe() text — it overlays a zod-inferred type's runtime object- and field-level descriptions onto the generated schema, so zod schemas remain the single source of truth for documentation
(public) types: documented the inline telemetry payload fields of MonitoringLogData, so the telemetry data shapes in the generated public event schema now carry field descriptions
(public) events: Device.DeviceConnectionAdded and Device.DeviceConnectionDeleted are now classified as kind: telemetry (not domain) in the generated public event schema, matching their delivery over the telemetry stream
(public) events: DeviceTelemetryRecordUpdated (all versions and its V1→V2→V3 upcasters) now constrains its telemetry-name parameter to DeviceTelemetryType and its payload to MonitoringLogData[N], instead of the previously widened string/unknown; consumers must instantiate it with concrete telemetry types
(public) isMfaRequired field on CreateCompany command and CompanyCreated event (defaults to true), propagated through the platform consumer and persisted on the MongoDB organization model on company creation
(public) hasMfaRequired field accepted in account.settings via the ChangeAccountSettings command and the account API settings object
(public) events: deviceIdentityHash (the public, server-generated device id) added to the exported events Device.Verification.DevicePaired, Device.Verification.DeviceUnpaired, Device.DeviceConnectionAdded, Device.DeviceConnectionDeleted and Device.Telemetry.DeviceTelemetryRecordUpdated (all bumped to _rev: 3). The V2 → V3 upcasters inject deviceIdentityHash: null for historical events.
(public) events: SENSITIVE_FIELDS_BY_EXPORTED_EVENT — a type-safe, exhaustive map (keyed by AllowedEventType) of the sensitive payload fields to strip per exported event, plus the stripSensitiveFields runtime helper. Single source of truth for both event-stream-distributor payload censoring and public-doc generation. Currently strips deviceUid from the six device-identified exported events.
(public) command-handler: handlePairDeviceWithOrganizationAggregate (DevicePaired) and deviceDeprovisionAggregate (DeviceUnpaired) resolve the device and set deviceIdentityHash on emit, throwing if the device is missing.
(public) events: the public event schema generator (generateEventSchemas.ts) now censors the strip-map fields from the public schema only (events-schema-public.json); the internal schema keeps them. Fixed the @public tag placement on DevicePaired/DeviceUnpaired so these exported events are now included in the public schema.
(public) Bump @signageos/amqp to 0.13.x, which replaces the unbounded rejectedTimestamps Map with an LRU+TTL cache, fixing a memory leak in locked event consumption (CU-86c9c16yg)
(public) api: POST /v1/device/verification now accepts an optional deviceName and policy to set on the device during pairing
(public) Firmware versions support multiple device types: deviceTypes array replaces the single deviceType so one firmware record covers all compatible devices per (applicationType, version); new UpdateFirmwareVersionDeviceTypes command + FirmwareVersionDeviceTypesUpdated event allow updating device types of an existing firmware version; migration consolidates duplicated firmware records into one superset record
(public) api: GET /v1/company now accepts a uids query parameter to filter companies by uid (intersected with the caller's accessible companies, so it never widens access)
(public) api: Device telemetry history endpoint GET /v1/device/{deviceUid}/telemetry/{telemetryType} (and its /count companion) now accept since and until query parameters to filter records by createdAt (both inclusive, combinable)
(public) API: Gate Event Stream exporter configuration endpoints (list/count/get/create/update/delete) behind the new EventStreams organization feature entitlement — returns 402 when the organization lacks it
(public) API: Event Stream exporter responses now include per-exporter delivery observability (exported/errors/retries counters, lastSuccessfulExportAt, and the most recent failures on single-config reads)
(public) types/user-domain-model: FROZEN_CONTENT content guard item type
(public) command-handler: Restrict FROZEN_CONTENT content guard items to categories with FROZEN_CONTENT evaluationStrategy (and forbid other item types in such categories)
(public) user-domain-model: Add deprecated field to content guard category model to support deprecating categories that are still referenced by alert rules
(public) api: Add fields deprecated and evaluationStrategy to managed content guard category
(public) Harmonize FeatureFlag to HiddenFeature and FeatureEntitlements — rename disabledFeatures to hiddenFeatures + hiddenFents in WhiteLabelSettings
(public) Allow CloseDataReporter command to run when the previous run's receivedAt differs by less than 1 second from the 24-hour window boundary (fixes daily data-reporter skip due to millisecond timing jitter)
(public) Platform Consumer no longer crashes on DeviceTelemetryRecordUpdated events with data: null; null telemetry is treated as non-compliant when evaluating policy status
(public) Action log now records keepAppletRunning for proprietary timer changes, so the action-log row reflects the setting immediately instead of only after the device reports back
(public) EventsInvalidatedCache passes correct options when creating rejected queues. This fixes the issues of the rejected queues being created as durable when they should be transient. This bug led to queues not being deleted when the service restarts, which caused a substantial load to RabbitMQ.
(public) Fix platform-consumer event consumption race condition for Location CRUD and device assign/unassign by switching to bindEventBatch with organization/location lock keys
(public) Add V2 Location events (LocationDeleted, LocationArchived, LocationUnarchived) and V3 device-location events (DeviceAssignedToLocation, DeviceUnassignedFromLocation) with organizationUid for proper event versioning
(public) Location assign/unassign to/from device is now idempotent
(public) command-handler: Allow users to be added to multiple companies without admin privileges
(public) Better errors for firmware creation and publishing.
(public) Added firmwareCommandModel to CQRS command models factory
(public) Added semverVersion, deviceType, and description fields to firmware version entity
(public) GET /v2/firmware new endpoint for listing firmware versions with advanced filtering (applicationType, semverVersion, deviceType, prefix/suffix matching)
(public) GET /v2/firmware/count new endpoint for counting firmware versions
(public) GET /v2/firmware/:firmwareVersionUid new endpoint for getting a single firmware version
Feature Entitlements — certain API endpoints now require the organization or company to have the appropriate entitlement; requests without it receive a 402 response
Support for Auth0 namespaced claims (https://signageos.io/sosAccountId) in JWT authentication, enabling CLI tools using Auth0 Device Flow
JWT auth fallback in organization-authenticated endpoints — when no clientId:secret is found, the middleware now verifies JWT tokens and resolves the organization from organizationUid query/body parameter
GET /v1/organization-tag returns assignedContentGuardItemsCount in response
PUT /v1/content-guard/item/{contentGuardItemUid}/tag/{tagUid} and DELETE /v1/content-guard/item/{contentGuardItemUid}/tag/{tagUid} endpoints for assigning and removing tags
Endpoint POST /v1/content-guard/ai-helper/upload-temp-image
Endpoint POST /v1/content-guard/ai-helper/generate/description
Endpoint POST /v1/content-guard/ai-helper/generate/prompt
Endpoint POST /v1/content-guard/ai-helper/finalize
GET /v1/js-api-version endpoint to get list of all available JS API versions
GET /v2/device now returns featureFlags field in device resources when set
GET /v2/device/:deviceUid now returns featureFlags field in device resource when set
PUT /v2/device/:deviceUid now accepts featureFlags field to enable/disable device features (screenshotCapture, systemLogs, cpuUsage, memoryUsage, customScripts, plugins, runners)
PUT /v1/account endpoint now supports updating favorites
accessLevel field to account resource in GET /v1/account
Connection to API DragonFly Redis database for request rate limiting and counting
New middleware that automatically detects white label settings by hostname and Auth0 status from JWT tokens, injecting client context into API requests
Integrated CQRS command models pattern into API endpoints with automatic client context injection and mock command dispatcher for testing
Endpoint /v1/device/:deviceUid/offline-range is no longer available in favor to /v1/device/:deviceUid/telemetry/latest, /v1/device/telemetry/latest or /v1/device/:deviceUid/telemetry/ONLINE_STATUS
Revert "Endpoint GET /v1/organization now supports account, organization and jwtToken authentication" due to increased number of 400 responses in production. Needs further investigation.
Improved error handling for policy creation and updates with encrypted values
Endpoint POST /v1/device/:deviceUid/connect - refactored to support account, organization and jwtToken authentication
Endpoint POST /v1/device/:deviceUid/disconnect - refactored to support account, organization and jwtToken authentication
Upgrade lib to v21.1.3 - Application won't exit if Redis connection is lost for a while. It will keep trying to reconnect forever or it apply provided retryStrategy instead
respond with SERVICE_UNAVAILABLE on redis error
Endpoint GET /v1/organization now supports account, organization and jwtToken authentication
Endpoint PUT /v1/device/:deviceUid/application/:applicationType/version - check that requested Android application version is not older than current application version (Android doesn't support downgrading applications)
Endpoint POST /v1/bulk-operation no longer produces an error about invalid body properties. Previously, it incorrectly required deviceIdentityHash to be included in the request body.
Endpoint POST /v1/:deviceUid/deprovision refactored to use new ACL
Endpoint GET /v1/device/:deviceUid/custom-script supports pagination, sorting and filtering by customScriptUids and versions
Endpoint DELETE /v1/device/:deviceUid/custom-script/:customScriptUid supports query parameter deleteVersions to delete all versions of the custom script as well
Endpoint GET /v1/device/:deviceUid/custom-script/:customScriptUid/version supports filtering by platforms
Endpoint GET /v1/device/:deviceUid/custom-script/:customScriptUid/version returns publishedAt and deprecatedAt
New endpoint GET /v1/device/:deviceUid/custom-script/count
New endpoint GET /v1/device/:deviceUid/custom-script/latest
Endpoint POST /v1/custom-script/:customScriptUid/version/:version/platform/:platform/archive converts base64 encoded md5 checksum to hexadecimal in the final file path.
Endpoints related to Device Telemetry are now loaded from MongoDB database instead of InfluxDB. This change is transparent to the user and should not affect the functionality of the API.
Calculating device current time for devices without specified timezone
Update privileges needed for endpoint POST /v1/policy/
Update privileges needed for endpoint PUT /v1/policy/
Occasional 404 on GET /v1/emulator after POST /v1/emulator call. (It was responding sooner than the emulator was written to the database)
Remove privilege check for reading the company. There are cases where a user needs access to a company's organizations but is only a guest in the parent company. The previous ACL check prevented this type of access. Removing the rule fixes the issue.
ACL provider respects company owner and company manager user roles and allows them to manage company child organizations entities for account based authentication
Endpoint POST /v1/policy allows organizationUid in body even if authenticated with organization token, as long as it matches the authenticated organization