Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt

Use this file to discover all available pages before exploring further.

After registration, every outbound request to the Synheart cloud must be signed by the device’s hardware key. This is the SDK-side reference for RFC-AUTH-MOBILE-0001 §8.

Public API

signRequest(
  appId: String,
  method: String,
  path: String,
  bodyBytes: ByteArray,
) -> SignedHeaders
Dart, Kotlin, and Swift expose the same shape. The SDK generates the timestamp and nonce internally.

Message construction (normative)

The signed message MUST be:
method          = uppercase HTTP method, ASCII   (e.g. "POST")
path            = request path, ASCII             (e.g. "/ingest/v1/hsi")
                  no query string
timestamp_str   = Unix seconds, ASCII decimal     (e.g. "1709312345")
raw_body_bytes  = exact HTTP request body bytes on the wire
                  (empty bytes for GET / no body)
The bytes signed:
message = method || "\n" || path || "\n" || timestamp_str || "\n" || raw_body_bytes
signature = ECDSA-P256-SIGN(private_key, SHA256(message))
For Synheart Core’s cloud ingest, the runtime strips the /ingest/ prefix from POSTs to /ingest/v1/... so signatures cover /v1/.... If you build clients that talk to those endpoints from outside Core, mirror this prefix-stripping rule.
The signature is encoded as Base64(ASN.1 DER). The runtime native bridge accepts compact r||s (64 bytes) from its sign_bytes callback and the SDK wraps that in DER for the wire header.

Six required headers

HeaderValueNotes
X-App-IDstringThe app_id provisioned in the dashboard.
X-Device-IDUUIDIssued at registration.
X-Synheart-SignatureBase64(ASN.1 DER)ECDSA-P256 over SHA256(message).
X-Synheart-TimestampUnix secondsDecimal ASCII, e.g. "1709312345".
X-Synheart-NonceUUID v4Unique per request, never reused.
X-Synheart-Sig-Version"1"Signature scheme version. Future evolution may bump.
The Dart SignedHeaders.toMap() returns exactly these six keys.

Nonce policy

  • UUID v4. Generated fresh per request; never reused across requests for the same (app_id, device_id).
  • Server enforces replay protection on write endpoints (POST, PUT, PATCH, DELETE) within the 300-second freshness window.
  • Replay protection is RECOMMENDED for read endpoints.
  • Server stores nonces with a 300-second TTL keyed by (device_id, nonce).

Timestamp policy

  • Server-side freshness window: 300 seconds.
  • SDK clock may drift; correctClockSkew(serverTimestamp) learns an offset from CLOCK_SKEW errors and applies it on subsequent signs.
  • The SDK persists clock_offset_ms in local storage.

Server-side verification (RFC §8.4)

Implementors of services that verify these signatures (auth-service, consent-service, ingest-service) follow:
  1. Extract the six headers; reject if any required header is missing.
  2. Reject if abs(server_time - X-Synheart-Timestamp) > 300.
  3. For write endpoints, reject if (device_id, nonce) has been seen in the freshness window.
  4. Reconstruct the message: method + "\n" + path + "\n" + timestamp + "\n" + body_bytes.
  5. Look up the public key by (app_id, device_id).
  6. Verify ECDSA P-256 signature over SHA256(message).
  7. Store the nonce with a 300-second TTL (write endpoints).

SignedHeaders type

class SignedHeaders {
  final String appId;
  final String deviceId;
  final String signature;
  final String timestamp;
  final String nonce;
  final String signatureVersion;  // default "1"

  Map<String, String> toMap();
}
Kotlin and Swift mirror this.

Sig-Version evolution

X-Synheart-Sig-Version exists so the server can support multiple schemes during migrations. Current value is "1". Bumping requires:
  • A new SignedHeaders.signatureVersion value emitted by the SDK.
  • Server-side support for the new scheme until the previous version is fully retired.
  • A capability response telling the SDK to upgrade.

Wiring through Synheart Core

When synheart-core consumes synheart-auth:
  1. Core constructs a DeviceAuthProvider over SynheartAuth.
  2. Core registers the auth provider’s crypto callbacks with the Synheart runtime.
  3. The runtime’s request-signing path builds X-Synheart-Proof (a compact JWS bound to the request URL+method) for HSI ingest. This is a different header from X-Synheart-Signature — both are hardware-backed by the same ECDSA P-256 key but cover different surfaces.
  4. For non-runtime traffic, hosts call SynheartAuth.signRequest(...) directly and attach the six headers themselves.
HeaderUsed by
X-Synheart-Signature (this SDK)Direct API calls from your app to Synheart endpoints.
X-Synheart-Proof (runtime)HSI ingest from the Synheart runtime. See Cloud Protocol.

What is not signed

  • Query string components (signing is over path only). Servers that include sensitive parameters in query strings are responsible for separate validation.
  • Headers other than the six listed above. The body and the path/method are the only signed surface.

Logging policy

Allowed (prod)Never (prod)
app_idRaw signatures
device_id (truncated/hashed)Raw request bodies
Error codesAttestation proof payloads
Latency metricsPublic key material (debug builds only with opt-in)
Rotation eventsClock offset values
State transitions
  • Registration — how the public key reaches the server.
  • ErrorsCLOCK_SKEW, NONCE_REPLAY, KEY_INVALIDATED recovery.
  • Cloud ProtocolX-Synheart-Proof for HSI ingest.