io7m | single-page | multi-page | epub | Stonesignal User Manual 1.0.0-SNAPSHOT

Stonesignal User Manual 1.0.0-SNAPSHOT

DATE 2025-04-14T08:56:15+00:00
DESCRIPTION User manual for the stonesignal server.
IDENTIFIER e481cb42-6e0a-4c29-881a-914a16b0c400
LANGUAGE en
SOURCE https://www.io7m.com/software/stonesignal/
TITLE Stonesignal User Manual 1.0.0-SNAPSHOT
The stonesignal package provides a server for recording device location data.

1.2. Features

  • Record GPS and other location information from devices running an appropriate client.
  • Full API access for all operations.
  • Complete audit log; every operation that changes the state of the system is logged in an append-only log.
  • Fully instrumented with OpenTelemetry.
  • A small, easily auditable codebase.
  • An extensive automated test suite.
  • Platform independence. No platform-dependent code is included in any form, and installations can largely be carried between platforms without changes.
  • OSGi-ready
  • JPMS-ready
  • Support for Canonmill keystores.
  • ISC license.
The stonesignal server package is available from the following sources:
Regardless of the distribution method, the stonesignal package will contain a command named stonesignal that acts as the main entrypoint to all of the server and client functionality.
The stonesignal server requires a PostgreSQL server. The stonesignal server will create the required tables and database objects on first startup, given the name of a running PostgreSQL database, and a PostgreSQL role and password.
A distribution package can be found at Maven Central.
The stonesignal command requires that a Java 21+ compatible JVM be accessible via /usr/bin/env java.
Verify the integrity of the distribution zip file:

2.3.4. Verify

$ gpg --verify stonesignal-distribution.zip.asc
gpg: assuming signed data in 'stonesignal-distribution.zip.asc'
gpg: Signature made Tue 28 Jun 2022 15:01:56 GMT
gpg:                using RSA key 12BC 7CF4 BB72 BD17 F7F5  01EB 3A1B 34F8 9D7A D0FC
gpg:                issuer "contact@io7m.com"
gpg: using pgp trust model
gpg: Good signature from "io7m.com (2025 github-ci-maven-rsa-key) <contact@io7m.com>" [unknown]
Unzip the zip file, and set up the environment appropriately. The stonesignal command expects an environment variable named STONESIGNAL_HOME to be defined that points to the installation directory.

2.3.6. Extract

$ unzip com.io7m.stonesignal.main-1.0.0-SNAPSHOT-distribution.zip
$ export STONESIGNAL_HOME=$(realpath stonesignal)
$ ./stonesignal/bin/stonesignal
stonesignal: usage: stonesignal [command] [arguments ...]
...
OCI images are available from Quay.io for use with podman or docker.

2.4.1.2. Podman/Docker

$ podman pull quay.io/io7mcom/stonesignal:1.0.0-SNAPSHOT
$ podman run quay.io/io7mcom/stonesignal:1.0.0-SNAPSHOT
stonesignal: usage: stonesignal [command] [arguments ...]
...
The stonesignal package uses PostgreSQL for all persistent data.
Create an owner role in PostgreSQL that will own the database objects:

2.5.2.2. Owner Role Creation

postgres=# create role stonesignal_owner with login password 'a very strong password';
Create a new database owned by the owner role:

2.5.2.4. Database Creation

postgres=# create database stonesignal with owner stonesignal_owner;
The stonesignal package sets up multiple roles during database initialization. The configured roles have different degrees of privileges in order to allow, for example, external systems such as database metrics collectors read-only access to the database. All the defined rules are declared with the built-in PostgreSQL restrictions such as nocreatedb, nocreaterole, etc.
During the startup of the stonesignal server, the server will connect to the database using the owner role and do any database table initialization and/or schema upgrades necessary. The server will then disconnect from the database, and then connect to the database again using the worker role. The worker role is then used for normal operation of the server; if this role is somehow compromised, the role only has a limited ability to do any damage to the database, and cannot affect the audit log at all.
The owner role is the role that owns the database and is permitted to create tables, create new roles, etc. This role is used by the stonesignal package when creating the database during the first run of the server, and for upgrading database schemas later. Administrators are free to pick the name of the role, although it is recommended that the role be named stonesignal_owner to make it clear as to the purpose of the role. The stonesignal package will create the other roles it uses for normal operation automatically, and will manage their passwords based on the server configuration.
If the PostgreSQL OCI image is used, it is common to have the image create this role automatically using the POSTGRES_USER and POSTGRES_PASSWORD variables:

2.5.3.2.3. Example

$ podman run \
  --name some-postgres \
  -e POSTGRES_USER=stonesignal_owner \
  -e POSTGRES_PASSWORD=mysecretpassword \
  -d postgres
The worker role is the role that is used for normal database operation. It is a role that has read/write access to all tables (except for the audit log which is restricted to being append-only), although it is not granted the ability to create new tables, drop tables, or do other schema manipulation. The role is always named stonesignal, and adminstrators are required to set a password for this role.
The reader role is a role that is permitted read-only access to some of the database. Specifically, the role does not have access to columns containing secrets such as device keys. The role is therefore safe for use by external systems that want to connect to the database directly to do data analysis. The role is always named stonesignal_reader, and adminstrators are required to set a password for this role.
The device role is a role that is permitted update access to some of the database. The role is always named stonesignal_device, and adminstrators are required to set a password for this role.
The server can now be run with stonesignal server:

2.6.1.2. Run

$ stonesignal server --configuration server.conf
stonesignal: INFO: com.io7m.stonesignal.server.device_api_v1.StDeviceAPI: [localhost/<unresolved>:10000] Device API server started
stonesignal: INFO: com.io7m.stonesignal.server.admin_api_v1.StAdminAPI: [localhost/<unresolved>:10001] Admin API server started
stonesignal: INFO: com.io7m.stonesignal.server.data_api_v1.StDataAPI: [localhost/<unresolved>:10002] Data API server started
The server does not fork into the background and is designed to be run under process supervision.

Footnotes

1
If running under podman or docker, remember to use the -i and -t options.
The stonesignal server is configured using a single JSON-formatted configuration file. The format has a fully documented schema and so configuration files can be independently validated, and benefit from autocompletion in most modern IDEs.
Configuration files are allowed to contain line-based // style comments.
The smallest working configuration file, assuming a database at db.example.com:

3.2.2. Example

{
  // Line-based comments are permitted.
  "Database": {
    "Kind": "POSTGRESQL",
    "OwnerRole": "stonesignal_owner",
    "OwnerPassword": "exampleownerpassword",
    "WorkerPassword": "exampleworkerpassword",
    "ReaderPassword": "examplereaderpassword",
    "DevicePassword": "exampledevicepassword",
    "Address": "db.example.com",
    "Port": 5432,
    "Name": "stonesignal",
    "Upgrade": true
  },
  "DeviceAPI": {
    "Host": "localhost",
    "Port": 10000
  },
  "AdminAPI": {
    "Host": "localhost",
    "Port": 10001,
    "APIKey": "1DE48CA6D6A5C172B259850D9A060AE9E51BDB2DF28CEDB19DFF1A4DDCC3521F"
  },
  "DataAPI": {
    "Host": "localhost",
    "Port": 10002,
    "APIKey": "164FA81EFDC9D27C4FD7BD0875C0C4702AE75D9DB9148BBD79EE0B6E531EA7A2"
  }
}
The DeviceAPI section of the configuration file configures the device API service.
The AdminAPI section of the configuration file configures the admin API service.
The DataAPI section of the configuration file configures the data API service.
The Database section of the configuration file configures the database.
The OwnerRole property specifies the name of the role that owns the database. Conventionally, this should be stonesignal_owner, but can be set independently by the database administrator.
The OwnerRolePassword property specifies the password of the owner role.
The WorkerRolePassword property specifies the password of the worker role used for normal database operation.
The ReaderRolePassword property specifies the password of the reader role used for read-only database access.
The DeviceRolePassword property specifies the password of the device role.
The Name property specifies the database name.
The Upgrade property specifies that the database schema should be upgraded on startup.
An example database configuration:

3.6.2.2. Example

"Database": {
  "Kind": "POSTGRESQL",
  "OwnerRole": "stonesignal_owner",
  "OwnerPassword": "exampleownerpassword",
  "WorkerPassword": "exampleworkerpassword",
  "ReaderPassword": "examplereaderpassword",
  "DevicePassword": "exampledevicepassword",
  "Address": "db.example.com",
  "Port": 5432,
  "Name": "stonesignal",
  "Upgrade": true,
  "MinimumConnections": 1,
  "MaximumConnections": 10
}
The OpenTelemetry section of the configuration file configures Open Telemetry. This section is optional and telemetry is disabled if the section is not present.
The logical service name should be provided in the ServiceName property.
If the OpenTelemetry object contains a Traces object, OTLP traces will be sent to a specified endpoint. The Endpoint property specifies the endpoint, and the Protocol property must be HTTP.
If the OpenTelemetry object contains a Metrics object, OTLP metrics will be sent to a specified endpoint. The Endpoint property specifies the endpoint, and the Protocol property must be HTTP.
If the OpenTelemetry object contains a Logs object, OTLP logs will be sent to a specified endpoint. The Endpoint property specifies the endpoint, and the Protocol property property must be HTTP.
An example Open Telemetry configuration:

3.7.5.2. Example

"OpenTelemetry": {
  "ServiceName": "stonesignal01",
  "Logs": {
    "Endpoint": "http://logs.example.com:4318/v1/logs",
    "Protocol": "HTTP"
  },
  "Metrics": {
    "Endpoint": "http://metrics.example.com:4318/v1/metrics",
    "Protocol": "HTTP"
  },
  "Traces": {
    "Endpoint": "http://traces.example.com:4318/v1/traces",
    "Protocol": "HTTP"
  }
}
The JSON schema for the configuration file is as follows:

3.8.2. Configuration Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "AdminAPI" : {
      "type" : "object",
      "properties" : {
        "APIKey" : {
          "type" : "string",
          "description" : "The API key."
        },
        "Host" : {
          "type" : "string",
          "description" : "The host address to which to bind."
        },
        "Port" : {
          "type" : "integer",
          "format" : "int32",
          "description" : "The host port to which to bind."
        },
        "TLS" : {
          "$ref" : "#/$defs/TLS-nullable",
          "description" : "The TLS configuration."
        }
      },
      "required" : [ "APIKey", "Host", "Port" ]
    },
    "DataAPI" : {
      "type" : "object",
      "properties" : {
        "APIKey" : {
          "type" : "string",
          "description" : "The API key."
        },
        "Host" : {
          "type" : "string",
          "description" : "The host address to which to bind."
        },
        "Port" : {
          "type" : "integer",
          "format" : "int32",
          "description" : "The host port to which to bind."
        },
        "TLS" : {
          "$ref" : "#/$defs/TLS-nullable",
          "description" : "The TLS configuration."
        }
      },
      "required" : [ "APIKey", "Host", "Port" ]
    },
    "Database" : {
      "type" : "object",
      "properties" : {
        "Address" : {
          "type" : "string",
          "description" : "The database address."
        },
        "DevicePassword" : {
          "type" : "string",
          "description" : "The database device role password."
        },
        "Kind" : {
          "$ref" : "#/$defs/DatabaseKind",
          "description" : "The database kind."
        },
        "Name" : {
          "type" : "string",
          "description" : "The database name."
        },
        "OwnerPassword" : {
          "type" : "string",
          "description" : "The database owner role password."
        },
        "OwnerRole" : {
          "type" : "string",
          "description" : "The database owner role."
        },
        "Port" : {
          "type" : "integer",
          "format" : "int32",
          "description" : "The database port."
        },
        "ReaderPassword" : {
          "type" : "string",
          "description" : "The database reader role password."
        },
        "Upgrade" : {
          "type" : "boolean",
          "description" : "Upgrade the database?"
        },
        "WorkerPassword" : {
          "type" : "string",
          "description" : "The database worker role password."
        }
      },
      "required" : [ "Address", "DevicePassword", "Kind", "Name", "OwnerPassword", "OwnerRole", "Port", "ReaderPassword", "Upgrade", "WorkerPassword" ]
    },
    "DatabaseKind" : {
      "type" : "string",
      "const" : "POSTGRESQL"
    },
    "DeviceAPI" : {
      "type" : "object",
      "properties" : {
        "Host" : {
          "type" : "string",
          "description" : "The host address to which to bind."
        },
        "Port" : {
          "type" : "integer",
          "format" : "int32",
          "description" : "The host port to which to bind."
        },
        "TLS" : {
          "$ref" : "#/$defs/TLS-nullable",
          "description" : "The TLS configuration."
        }
      },
      "required" : [ "Host", "Port" ]
    },
    "Logs" : {
      "type" : "object",
      "properties" : {
        "Endpoint" : {
          "type" : "string",
          "format" : "uri",
          "description" : "The endpoint address for OTLP data."
        },
        "Protocol" : {
          "$ref" : "#/$defs/OTLPProtocol",
          "description" : "The protocol used to deliver OTLP data."
        }
      },
      "required" : [ "Endpoint", "Protocol" ]
    },
    "Logs-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/Logs"
      } ]
    },
    "Metrics" : {
      "type" : "object",
      "properties" : {
        "Endpoint" : {
          "type" : "string",
          "format" : "uri",
          "description" : "The endpoint address for OTLP data."
        },
        "Protocol" : {
          "$ref" : "#/$defs/OTLPProtocol",
          "description" : "The protocol used to deliver OTLP data."
        }
      },
      "required" : [ "Endpoint", "Protocol" ]
    },
    "Metrics-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/Metrics"
      } ]
    },
    "OTLPProtocol" : {
      "type" : "string",
      "const" : "HTTP"
    },
    "OpenTelemetry" : {
      "type" : "object",
      "properties" : {
        "Logs" : {
          "$ref" : "#/$defs/Logs-nullable",
          "description" : "The configuration for logs."
        },
        "Metrics" : {
          "$ref" : "#/$defs/Metrics-nullable",
          "description" : "The configuration for metrics."
        },
        "ServiceName" : {
          "type" : "string",
          "description" : "The logical service name."
        },
        "Traces" : {
          "$ref" : "#/$defs/Traces-nullable",
          "description" : "The configuration for traces."
        }
      },
      "required" : [ "ServiceName" ]
    },
    "OpenTelemetry-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/OpenTelemetry"
      } ]
    },
    "Path" : {
      "type" : "object"
    },
    "TLS" : {
      "type" : "object",
      "properties" : {
        "KeyStore" : {
          "$ref" : "#/$defs/TLSStore",
          "description" : "The key store."
        },
        "TrustStore" : {
          "$ref" : "#/$defs/TLSStore",
          "description" : "The trust store."
        }
      },
      "required" : [ "KeyStore", "TrustStore" ]
    },
    "TLS-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/TLS"
      } ]
    },
    "TLSStore" : {
      "type" : "object",
      "properties" : {
        "Password" : {
          "type" : "string",
          "description" : "The store password."
        },
        "Path" : {
          "$ref" : "#/$defs/Path",
          "description" : "The store path."
        },
        "Provider" : {
          "type" : "string",
          "description" : "The store provider."
        },
        "Type" : {
          "type" : "string",
          "description" : "The store type."
        }
      },
      "required" : [ "Password", "Path", "Provider", "Type" ]
    },
    "Traces" : {
      "type" : "object",
      "properties" : {
        "Endpoint" : {
          "type" : "string",
          "format" : "uri",
          "description" : "The endpoint address for OTLP data."
        },
        "Protocol" : {
          "$ref" : "#/$defs/OTLPProtocol",
          "description" : "The protocol used to deliver OTLP data."
        }
      },
      "required" : [ "Endpoint", "Protocol" ]
    },
    "Traces-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/Traces"
      } ]
    }
  },
  "type" : "object",
  "properties" : {
    "AdminAPI" : {
      "$ref" : "#/$defs/AdminAPI",
      "description" : "The admin API configuration."
    },
    "DataAPI" : {
      "$ref" : "#/$defs/DataAPI",
      "description" : "The data API configuration."
    },
    "Database" : {
      "$ref" : "#/$defs/Database",
      "description" : "The database configuration."
    },
    "DeviceAPI" : {
      "$ref" : "#/$defs/DeviceAPI",
      "description" : "The device API configuration."
    },
    "OpenTelemetry" : {
      "$ref" : "#/$defs/OpenTelemetry-nullable",
      "description" : "The OpenTelemetry configuration."
    }
  },
  "required" : [ "AdminAPI", "DataAPI", "Database", "DeviceAPI" ]
}
The stonesignal package provides a server-based application to record location data sent from devices.
This section of the documentation describes the internal stonesignal model.
A device is a piece of GPS (or other location technology) enabled electronic equipment. In the typical use case, a device will be a smartphone.
Devices are assigned device IDs. A device ID is a public UUID value that identifies the device.
Devices are assigned device keys. A device key is a secret 32-byte random value that the device uses to authenticate itself to the device API.
The text encoding for a device key is given by the pattern [A-F0-9]{64}. So, for example, the string 02210D83680AC0F4A50C47F51DCCE35610DE59717623A0D9408067279A63D511 is a well-formed device key, whilst 02210d83680ac0f4a50c47f51dcce35610de59717623a0d9408067279a63d511 is not.
The device API is the interface exposed to devices. The device API serves only one purpose: It accepts location updates from devices and records them into the database.
The admin API is the interface exposed to administrators. The API allows for enrolling and editing devices.
The data API is the interface exposed to data analysis applications. It provides a read-only view of devices and locations, but it deliberately does not have access to secret values such as device keys.
The server maintains an append-only audit log consisting of a series of audit events. An audit event has an integer id, an owner (represented by an account UUID), a timestamp, a type, and a message consisting of a set of key/value pairs.
Each operation that changes the underlying database typically results in an event being logged to the audit log.
The stonesignal package is extensively instrumented with OpenTelemetry in order to allow for the server to be continually monitored. The package publishes metrics, logs, and traces, all of which can be independently enabled or disabled. Most installations will only want to enable metrics or logs in production; traces are more useful when trying to diagnose performance problems, or for doing actual development on the stonesignal package.
The package publishes the following metrics that can be used for monitoring:

5.2.1.2. Metrics

Name Description
stonesignal_up A gauge that displays a constant 1 value while the server is up.
stonesignal_http_time A gauge that logs the time each HTTP request has taken in nanoseconds.
stonesignal_http_requests A counter that is incremented every time an HTTP request is handled.
stonesignal_http_requests_size A counter that is incremented with the size of every HTTP request.
stonesignal_http_responses_size A counter that is incremented with the size of every produced HTTP response.
stonesignal_http_responses_2xx A counter that is incremented with every HTTP response that produces a 2xx status code.
stonesignal_http_responses_4xx A counter that is incremented with every HTTP response that produces a 4xx status code. A 4xx status code should be understood to mean "blame the client".
stonesignal_http_responses_5xx A counter that is incremented with every HTTP response that produces a 5xx status code. A 5xx status code should be understood to mean "blame the server".
The package may produce other metrics, however these are undocumented and should not be relied upon.
The stonesignal package provides a command-line interface for performing tasks such as starting the server, checking configuration files, and etc. The base stonesignal command is broken into a number of subcommands which are documented over the following sections.

6.1.2. Command-Line Overview

stonesignal: usage: stonesignal [command] [arguments ...]

  The stonesignal server command-line application.

  Use the "help" command to examine specific commands:

    $ stonesignal help help.

  Command-line arguments can be placed one per line into a file, and
  the file can be referenced using the @ symbol:

    $ echo help > file.txt
    $ echo help >> file.txt
    $ stonesignal @file.txt

  Commands:
    help          Show usage information for a command.
    server        Start the server.
    version       Show the application version.

  Documentation:
    https://www.io7m.com/software/stonesignal/
All subcommands accept a --verbose parameter that may be set to one of trace, debug, info, warn, or error. This parameter sets the lower bound for the severity of messages that will be logged. For example, at debug verbosity, only messages of severity debug and above will be logged. Setting the verbosity to trace level effectively causes everything to be logged, and will produce large volumes of debugging output.
The stonesignal command-line tool uses quarrel to parse command-line arguments, and therefore supports placing command-line arguments into a file, one argument per line, and then referencing that file with @. For example:

6.1.5. @ Syntax

$ stonesignal server --configuration server.conf

$ (cat <<EOF
--configuration
server.conf
EOF
) > args.txt

$ stonesignal @args.txt
All subcommands, unless otherwise specified, yield an exit code of 0 on success, and a non-zero exit code on failure.
server - Start the server
The server command starts the server.

6.2.3.1. --configuration

Attribute Value
Name --configuration
Type java.nio.file.Path
Default Value
Cardinality [1, 1]
Description The configuration file.

6.2.3.2. --verbose

Attribute Value
Name --verbose
Type com.io7m.quarrel.ext.logback.QLogLevel
Default Value info
Cardinality [1, 1]
Description Set the logging level of the application.

6.2.4.1. Example

$ stonesignal server --configuration server.conf
stonesignal: INFO: com.io7m.stonesignal.server.device_api_v1.StDeviceAPI: [localhost/<unresolved>:10000] Device API server started
stonesignal: INFO: com.io7m.stonesignal.server.admin_api_v1.StAdminAPI: [localhost/<unresolved>:10001] Admin API server started
stonesignal: INFO: com.io7m.stonesignal.server.data_api_v1.StDataAPI: [localhost/<unresolved>:10002] Data API server started
version - Display the package version
The version command displays the current version of the package.
The command has no parameters.

6.3.4.1. Example

stonesignal: usage: version

  Show the application version.

  The command does not accept any named arguments.

  The command does not accept any positional arguments.
  
  The version command produces the following output, in order:
  
    * The application ID (such as "com.io7m.quarrel")
    * The application version (such as "1.2.0")
    * The application build (such as "eacd59a2")
  
  For example, for a hypothetical application named "quarrel":
  
    $ quarrel version
    com.io7m.quarrel 1.2.0 eacd59a2
The admin API is the API used to accept administrative commands.
The API is designed as a set of HTTP endpoints with which clients exchange messages in a strictly-defined and versioned JSON (or, optionally, CBOR-encoded) format.
There is no version negotiation in the protocol. Clients must check to see which versions are available using the version document, and then speak directly to the versioned endpoints using the correct protocol version.
The API endpoints allow for communicating in a number of different formats that each use the same underlying data model. If a client specifies a Content-Type header with a request, the server will respond using the same format.
All API endpoints will accept a Content-Type header that must be one of the following values:

7.1.2.3. Format Values

Format Description
application/stonesignal+json The textual JSON format.
application/stonesignal+cbor The binary CBOR format.
If no Content-Type header is provided, application/stonesignal+json is assumed.
Clients must include an Authorization header with a value of Bearer k, where k is the configured API key.
All endpoints require authentication except for the root endpoint.

7.1.3.3. Example

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-get-by-id
All endpoints return a 200 HTTP status code on success with an endpoint-specific response message. On errors, all endpoints return an HTTP status code greater than or equal to 400 with a response conforming to the following schema:

7.1.4.2. Error Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "ErrorCode" : {
      "type" : "string",
      "description" : "The error code."
    },
    "Message" : {
      "type" : "string",
      "description" : "The human-readable error message."
    }
  },
  "required" : [ "ErrorCode", "Message" ]
}
As an example:

7.1.4.4. Error Schema

$ curl -s http://example.com/1/0/device-get-by-id | jq
{
  "ErrorCode": "error-401",
  "Message": "Unauthorized"
}
The / endpoint exposes a ventrad version document that explains which API versions are available and where they are hosted.
The Endpoint property of each protocol defines a prefix that must be included in order to reach the correct endpoint. So, for example, if the Endpoint property is /1/0, then the /device-get-by-id endpoint would be reachable at /1/0/device-get-by-id.
The endpoint ignores any request data.
The endpoint responds with a ventrad version document.

7.1.5.4.1. Example Request

$ curl -s http://example.com/ | jq
{
  "%Schema": "urn:com.io7m.ventrad:1",
  "Protocols": [
    {
      "Id": "com.io7m.stonesignal.admin",
      "VersionMajor": 1,
      "VersionMinor": 0,
      "Endpoint": "/1/0/",
      "Description": "Stonesignal 1.0 Admin API"
    }
  ]
}
The /device-get-by-id endpoint is used to retrieve the definition of a device by its ID.
The endpoint expects requests of the following type:

7.1.6.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "DeviceID" : {
      "type" : "string",
      "format" : "uuid",
      "description" : "The device ID."
    }
  },
  "required" : [ "DeviceID" ]
}
The endpoint responds with objects of the following type:

7.1.6.3.2. Response Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "Map(String,String)" : {
      "type" : "object"
    },
    "St1AdminDevice" : {
      "type" : "object",
      "properties" : {
        "DeviceID" : {
          "type" : "string",
          "format" : "uuid",
          "description" : "The device ID."
        },
        "DeviceKey" : {
          "type" : "string",
          "description" : "The device key."
        },
        "Metadata" : {
          "$ref" : "#/$defs/Map(String,String)",
          "description" : "The device metadata."
        },
        "Name" : {
          "type" : "string",
          "description" : "The device name."
        }
      },
      "required" : [ "DeviceID", "DeviceKey", "Name" ]
    },
    "St1AdminDevice-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/St1AdminDevice"
      } ]
    }
  },
  "type" : "object",
  "properties" : {
    "Device" : {
      "$ref" : "#/$defs/St1AdminDevice-nullable",
      "description" : "The device."
    }
  }
}

7.1.6.4.1. Example

$ cat data.json
{
  "DeviceID": "f4f3bdce-fb35-4158-b122-4d7c861c1b18"
}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-get-by-id
{
  "Device": {
    "DeviceID": "f4f3bdce-fb35-4158-b122-4d7c861c1b18",
    "DeviceKey": "0D00B8679D5CA37A731BBA2575AD259E9E0309517E16010A0ED55792E5C6D56D",
    "Name": "Fake",
    "Metadata": {}
  }
}
The /device-get-by-key endpoint is used to retrieve the definition of a device by its key.
The endpoint expects requests of the following type:

7.1.7.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "DeviceKey" : {
      "type" : "string",
      "description" : "The device key."
    }
  },
  "required" : [ "DeviceKey" ]
}
The endpoint responds with objects of the following type:

7.1.7.3.2. Response Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "Map(String,String)" : {
      "type" : "object"
    },
    "St1AdminDevice" : {
      "type" : "object",
      "properties" : {
        "DeviceID" : {
          "type" : "string",
          "format" : "uuid",
          "description" : "The device ID."
        },
        "DeviceKey" : {
          "type" : "string",
          "description" : "The device key."
        },
        "Metadata" : {
          "$ref" : "#/$defs/Map(String,String)",
          "description" : "The device metadata."
        },
        "Name" : {
          "type" : "string",
          "description" : "The device name."
        }
      },
      "required" : [ "DeviceID", "DeviceKey", "Name" ]
    },
    "St1AdminDevice-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/St1AdminDevice"
      } ]
    }
  },
  "type" : "object",
  "properties" : {
    "Device" : {
      "$ref" : "#/$defs/St1AdminDevice-nullable",
      "description" : "The device."
    }
  }
}

7.1.7.4.1. Example

$ cat data.json
{
  "DeviceKey": "0D00B8679D5CA37A731BBA2575AD259E9E0309517E16010A0ED55792E5C6D56D"
}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-get-by-key
{
  "Device": {
    "DeviceID": "f4f3bdce-fb35-4158-b122-4d7c861c1b18",
    "DeviceKey": "0D00B8679D5CA37A731BBA2575AD259E9E0309517E16010A0ED55792E5C6D56D",
    "Name": "Fake",
    "Metadata": {}
  }
}
The /device-put endpoint is used to update or create the definition of a device.
The endpoint expects requests of the following type:

7.1.8.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "Map(String,String)" : {
      "type" : "object"
    },
    "St1AdminDevice" : {
      "type" : "object",
      "properties" : {
        "DeviceID" : {
          "type" : "string",
          "format" : "uuid",
          "description" : "The device ID."
        },
        "DeviceKey" : {
          "type" : "string",
          "description" : "The device key."
        },
        "Metadata" : {
          "$ref" : "#/$defs/Map(String,String)",
          "description" : "The device metadata."
        },
        "Name" : {
          "type" : "string",
          "description" : "The device name."
        }
      },
      "required" : [ "DeviceID", "DeviceKey", "Name" ]
    }
  },
  "type" : "object",
  "properties" : {
    "Device" : {
      "$ref" : "#/$defs/St1AdminDevice",
      "description" : "The device."
    }
  },
  "required" : [ "Device" ]
}
The endpoint responds with an empty object.

7.1.8.4.1. Example

$ cat data.json
{
  "Device": {
    "DeviceID": "f057fc17-07ef-4b89-8aa1-004eb992a364",
    "DeviceKey": "ACCD3D5D5C3864083E371BD1E161C6071C3A847943CCCA6A833CECC1FA67D30C",
    "Name": "Fake",
    "Metadata": {
      "Manufacturer": "Bad Phones LLC"
    }
  }
}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-put
{}
The device API is the API used to accept location data from devices.
The API is designed as a set of HTTP endpoints with which clients exchange messages in a strictly-defined and versioned JSON (or, optionally, CBOR-encoded) format.
There is no version negotiation in the protocol. Clients must check to see which versions are available using the version document, and then speak directly to the versioned endpoints using the correct protocol version.
The API endpoints allow for communicating in a number of different formats that each use the same underlying data model. If a client specifies a Content-Type header with a request, the server will respond using the same format.
All API endpoints will accept a Content-Type header that must be one of the following values:

7.2.2.3. Format Values

Format Description
application/stonesignal+json The textual JSON format.
application/stonesignal+cbor The binary CBOR format.
If no Content-Type header is provided, application/stonesignal+json is assumed.
Clients must include an Authorization header with a value of Bearer k, where k is the assigned device key.
All endpoints require authentication except for the root endpoint.

7.2.3.3. Example

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 02210D83680AC0F4A50C47F51DCCE35610DE59717623A0D9408067279A63D511' \
  http://example.com/1/0/device-location-put
All endpoints return a 200 HTTP status code on success with an endpoint-specific response message. On errors, all endpoints return an HTTP status code greater than or equal to 400 with a response conforming to the following schema:

7.2.4.2. Error Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "ErrorCode" : {
      "type" : "string",
      "description" : "The error code."
    },
    "Message" : {
      "type" : "string",
      "description" : "The human-readable error message."
    }
  },
  "required" : [ "ErrorCode", "Message" ]
}
As an example:

7.2.4.4. Error Schema

$ curl -s http://example.com/1/0/device-location-put | jq
{
  "ErrorCode": "error-401",
  "Message": "Unauthorized"
}
The / endpoint exposes a ventrad version document that explains which API versions are available and where they are hosted.
The Endpoint property of each protocol defines a prefix that must be included in order to reach the correct endpoint. So, for example, if the Endpoint property is /1/0, then the /device-location-put endpoint would be reachable at /1/0/device-location-put.
The endpoint ignores any request data.
The endpoint responds with a ventrad version document.

7.2.5.4.1. Example Request

$ curl -s http://example.com/ | jq
{
  "%Schema": "urn:com.io7m.ventrad:1",
  "Protocols": [
    {
      "Id": "com.io7m.stonesignal.device",
      "VersionMajor": 1,
      "VersionMinor": 0,
      "Endpoint": "/1/0/",
      "Description": "Stonesignal 1.0 Device API"
    }
  ]
}
The /device-location-put is used to submit a location update from a device.
The endpoint expects requests of the following type:

7.2.6.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "ACC" : {
      "type" : "number",
      "format" : "double",
      "description" : "The estimated horizontal accuracy radius in meters of this location."
    },
    "ALT" : {
      "type" : "number",
      "format" : "double",
      "description" : "The altitude of this location in meters above the WGS84 reference ellipsoid."
    },
    "BE" : {
      "type" : "number",
      "format" : "double",
      "description" : "The bearing at the time of this location in degrees. "
    },
    "BEACC" : {
      "type" : "number",
      "format" : "double",
      "description" : "The estimated bearing accuracy in degrees of this location."
    },
    "LAT" : {
      "type" : "number",
      "format" : "double",
      "description" : "The latitude in degrees."
    },
    "LONG" : {
      "type" : "number",
      "format" : "double",
      "description" : "The longitude in degrees."
    },
    "MSLALT" : {
      "type" : "number",
      "format" : "double",
      "description" : "The Mean Sea Level altitude of this location in meters."
    },
    "MSLALTACC" : {
      "type" : "number",
      "format" : "double",
      "description" : "The estimated Mean Sea Level altitude accuracy in meters."
    },
    "SP" : {
      "type" : "number",
      "format" : "double",
      "description" : "The speed at the time of this location in meters per second."
    },
    "SPACC" : {
      "type" : "number",
      "format" : "double",
      "description" : "The estimated speed accuracy in meters per second of this location."
    }
  }
}
The endpoint responds with an empty JSON object.

7.2.6.4.1. Example

$ cat data.json
{
  "ACC": 1.0,
  "ALT": 32.0,
  ...

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 02210D83680AC0F4A50C47F51DCCE35610DE59717623A0D9408067279A63D511' \
  http://example.com/1/0/device-location-put
{}
The data API is the API used to retrieve location data from the server. The data API is considered to be unprivileged, and therefore will not return any secrets such as device keys.
The API is designed as a set of HTTP endpoints with which clients exchange messages in a strictly-defined and versioned JSON (or, optionally, CBOR-encoded) format.
There is no version negotiation in the protocol. Clients must check to see which versions are available using the version document, and then speak directly to the versioned endpoints using the correct protocol version.
The API endpoints allow for communicating in a number of different formats that each use the same underlying data model. If a client specifies a Content-Type header with a request, the server will respond using the same format.
All API endpoints will accept a Content-Type header that must be one of the following values:

7.3.2.3. Format Values

Format Description
application/stonesignal+json The textual JSON format.
application/stonesignal+cbor The binary CBOR format.
If no Content-Type header is provided, application/stonesignal+json is assumed.
Clients must include an Authorization header with a value of Bearer k, where k is the configured API key.
All endpoints require authentication except for the root endpoint.

7.3.3.3. Example

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-get-by-id
All endpoints return a 200 HTTP status code on success with an endpoint-specific response message. On errors, all endpoints return an HTTP status code greater than or equal to 400 with a response conforming to the following schema:

7.3.4.2. Error Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "ErrorCode" : {
      "type" : "string",
      "description" : "The error code."
    },
    "Message" : {
      "type" : "string",
      "description" : "The human-readable error message."
    }
  },
  "required" : [ "ErrorCode", "Message" ]
}
As an example:

7.3.4.4. Error Schema

$ curl -s http://example.com/1/0/device-get-by-id | jq
{
  "ErrorCode": "error-401",
  "Message": "Unauthorized"
}
The / endpoint exposes a ventrad version document that explains which API versions are available and where they are hosted.
The Endpoint property of each protocol defines a prefix that must be included in order to reach the correct endpoint. So, for example, if the Endpoint property is /1/0, then the /device-get-by-id endpoint would be reachable at /1/0/device-get-by-id.
The endpoint ignores any request data.
The endpoint responds with a ventrad version document.

7.3.5.4.1. Example Request

$ curl -s http://example.com/ | jq
{
  "%Schema": "urn:com.io7m.ventrad:1",
  "Protocols": [
    {
      "Id": "com.io7m.stonesignal.admin",
      "VersionMajor": 1,
      "VersionMinor": 0,
      "Endpoint": "/1/0/",
      "Description": "Stonesignal 1.0 Admin API"
    }
  ]
}
The /devices endpoint retrieves the set of defined devices.
The endpoint expects requests of the following type:

7.3.6.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object"
}
The endpoint responds with objects of the following type:

7.3.6.3.2. Response Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "Map(UUID,String)" : {
      "type" : "object"
    }
  },
  "type" : "object",
  "properties" : {
    "Devices" : {
      "$ref" : "#/$defs/Map(UUID,String)",
      "description" : "The devices by ID."
    }
  },
  "required" : [ "Devices" ]
}

7.3.6.4.1. Example

$ cat data.json
{}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/devices
{
  "Devices": {
    "f4f3bdce-fb35-4158-b122-4d7c861c1b18": "Fake",
    "70c49b44-f315-4d21-a1ab-60321fdc4bb6": "Fake",
    "f057fc17-07ef-4b89-8aa1-004eb992a364": "Fake"
  }
}
The /device-get-by-id endpoint is used to retrieve the definition of a device by its ID.
The endpoint expects requests of the following type:

7.3.7.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "DeviceID" : {
      "type" : "string",
      "format" : "uuid",
      "description" : "The device ID."
    }
  },
  "required" : [ "DeviceID" ]
}
The endpoint responds with objects of the following type:

7.3.7.3.2. Response Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "Map(String,String)" : {
      "type" : "object"
    },
    "St1DataDevice" : {
      "type" : "object",
      "properties" : {
        "DeviceID" : {
          "type" : "string",
          "format" : "uuid",
          "description" : "The device ID."
        },
        "Metadata" : {
          "$ref" : "#/$defs/Map(String,String)",
          "description" : "The device metadata."
        },
        "Name" : {
          "type" : "string",
          "description" : "The device name."
        }
      },
      "required" : [ "DeviceID", "Metadata", "Name" ]
    },
    "St1DataDevice-nullable" : {
      "anyOf" : [ {
        "type" : "null"
      }, {
        "$ref" : "#/$defs/St1DataDevice"
      } ]
    }
  },
  "type" : "object",
  "properties" : {
    "Device" : {
      "$ref" : "#/$defs/St1DataDevice-nullable",
      "description" : "The device."
    }
  }
}

7.3.7.4.1. Example

$ cat data.json
{
  "DeviceID": "f4f3bdce-fb35-4158-b122-4d7c861c1b18"
}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-get-by-id
{
  "Device": {
    "DeviceID": "f4f3bdce-fb35-4158-b122-4d7c861c1b18",
    "Name": "Fake",
    "Metadata": {}
  }
}
The /device-locations endpoint is used to retrieve the most recent location of all devices.
The endpoint expects requests of the following type:

7.3.8.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object"
}
The endpoint responds with objects of the following type:

7.3.8.3.2. Response Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "Map(UUID,St1DataLocation)" : {
      "type" : "object"
    }
  },
  "type" : "object",
  "properties" : {
    "Locations" : {
      "$ref" : "#/$defs/Map(UUID,St1DataLocation)",
      "description" : "The device locations by ID."
    }
  },
  "required" : [ "Locations" ]
}

7.3.8.4.1. Example

$ cat data.json
{}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-locations
{
  "Locations": {
    "f4f3bdce-fb35-4158-b122-4d7c861c1b18": {
      "ID": 3,
      "Time": "2025-04-13T13:27:47.056167Z",
      "Accuracy": 0.0,
      "Altitude": 0.0,
      "Bearing": 0.0,
      "BearingAccuracy": 0.0,
      "Latitude": 0.0,
      "Longitude": 0.0,
      "MSLAltitude": 0.0,
      "MSLAltitudeAccuracy": 0.0,
      "Speed": 0.0,
      "SpeedAccuracy": 0.0
    }
  }
}
The /device-locations-history endpoint is used to retrieve a list of locations for a specific device at some given point in the past.
The endpoint expects requests of the following type:

7.3.9.2.2. Request Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "type" : "object",
  "properties" : {
    "Count" : {
      "type" : "integer",
      "format" : "int32",
      "description" : "The maximum number of events to return."
    },
    "DeviceID" : {
      "type" : "string",
      "format" : "uuid",
      "description" : "The device ID."
    },
    "TimeStart" : {
      "type" : "string",
      "format" : "date-time",
      "description" : "The start time."
    }
  },
  "required" : [ "Count", "DeviceID", "TimeStart" ]
}
The endpoint responds with objects of the following type:

7.3.9.3.2. Response Schema

{
  "$schema" : "https://json-schema.org/draft/2020-12/schema",
  "$defs" : {
    "St1DataLocation" : {
      "type" : "object",
      "properties" : {
        "Accuracy" : {
          "type" : "number",
          "format" : "double",
          "description" : "The estimated horizontal accuracy radius in meters of this location."
        },
        "Altitude" : {
          "type" : "number",
          "format" : "double",
          "description" : "The altitude of this location in meters above the WGS84 reference ellipsoid."
        },
        "Bearing" : {
          "type" : "number",
          "format" : "double",
          "description" : "The bearing at the time of this location in degrees. "
        },
        "BearingAccuracy" : {
          "type" : "number",
          "format" : "double",
          "description" : "The estimated bearing accuracy in degrees of this location."
        },
        "ID" : {
          "type" : "integer",
          "format" : "int64",
          "description" : "The location ID."
        },
        "Latitude" : {
          "type" : "number",
          "format" : "double",
          "description" : "The latitude in degrees."
        },
        "Longitude" : {
          "type" : "number",
          "format" : "double",
          "description" : "The longitude in degrees."
        },
        "MSLAltitude" : {
          "type" : "number",
          "format" : "double",
          "description" : "The Mean Sea Level altitude of this location in meters."
        },
        "MSLAltitudeAccuracy" : {
          "type" : "number",
          "format" : "double",
          "description" : "The estimated Mean Sea Level altitude accuracy in meters."
        },
        "Speed" : {
          "type" : "number",
          "format" : "double",
          "description" : "The speed at the time of this location in meters per second."
        },
        "SpeedAccuracy" : {
          "type" : "number",
          "format" : "double",
          "description" : "The estimated speed accuracy in meters per second of this location."
        },
        "Time" : {
          "type" : "string",
          "format" : "date-time",
          "description" : "The time the update was received."
        }
      },
      "required" : [ "ID", "Time" ]
    }
  },
  "type" : "object",
  "properties" : {
    "Locations" : {
      "description" : "The device locations.",
      "type" : "array",
      "items" : {
        "$ref" : "#/$defs/St1DataLocation"
      }
    }
  },
  "required" : [ "Locations" ]
}

7.3.9.4.1. Example

$ cat data.json
{
  "DeviceID": "f4f3bdce-fb35-4158-b122-4d7c861c1b18",
  "TimeStart": "2025-04-13T00:00:00+00:00",
  "Count": 1000
}

$ curl \
  --data-binary @data.json \
  --header 'Content-Type: application/stonesignal+json' \
  --header 'Authorization: Bearer 6890BF0E202343407CAF3FF100A118BDBB1BECABE343F3480E9A3DB0D9C889DC' \
  http://example.com/1/0/device-location-history
{
  "Locations": [
    {
      "ID": 1,
      "Time": "2025-04-13T11:58:41.266913Z",
      "Accuracy": 0.0,
      "Altitude": 0.0,
      "Bearing": 0.0,
      "BearingAccuracy": 0.0,
      "Latitude": 0.0,
      "Longitude": 0.0,
      "MSLAltitude": 0.0,
      "MSLAltitudeAccuracy": 0.0,
      "Speed": 0.0,
      "SpeedAccuracy": 0.0
    },
    {
      "ID": 2,
      "Time": "2025-04-13T13:27:42.179066Z",
      "Accuracy": 0.0,
      "Altitude": 0.0,
      "Bearing": 0.0,
      "BearingAccuracy": 0.0,
      "Latitude": 0.0,
      "Longitude": 0.0,
      "MSLAltitude": 0.0,
      "MSLAltitudeAccuracy": 0.0,
      "Speed": 0.0,
      "SpeedAccuracy": 0.0
    },
    {
      "ID": 3,
      "Time": "2025-04-13T13:27:47.056167Z",
      "Accuracy": 0.0,
      "Altitude": 0.0,
      "Bearing": 0.0,
      "BearingAccuracy": 0.0,
      "Latitude": 0.0,
      "Longitude": 0.0,
      "MSLAltitude": 0.0,
      "MSLAltitudeAccuracy": 0.0,
      "Speed": 0.0,
      "SpeedAccuracy": 0.0
    }
  ]
}
This section of the manual attempts to describe the security properties of the stonesignal server.
An attacker with access to the APIs could send specially crafted messages designed to exhaust server resources during parsing/validation of the messages.
All APIs exposed by the stonesignal server are defined in terms of simple, flat record types. The jackson parser is used to deserialize JSON and CBOR requests, and the dixmont package is used to prevent the deserialization of anything other than a fixed list of types.
Additionally, requests made to the device API and the data API have a hardcoded maximum size limit of 32768 octets.
io7m | single-page | multi-page | epub | Stonesignal User Manual 1.0.0-SNAPSHOT