Runtime protocol
What your software and the runtime say to each other. Six requests, one signed envelope, and a set of refusal codes your users can be told about in your own words.
The signed envelope
Cryptographically signed authorization responses, verified under a key you pin.
Every accepted answer has the same shape. The SDK verifies the signature under the key you pinned, then decodes the payload and checks that it answers the request it made: the same challenge, application and environment, a server timestamp within thirty seconds of the client's clock, and for heartbeat and logout the session that was asked about. Only then are the claims exposed.
{
"key_id": "v1",
"payload": "<base64url of the exact JSON bytes that were signed>",
"signature": "<base64url Ed25519 signature over those bytes>"
}The runtime is at https://auth.obsidian.rip; the same routes also answer at https://obsidian.rip/v1/auth/…. The current and any still-trusted keys are published at GET /v1/auth/keys so a rotation can be pinned ahead of time. Signing keys rotate with zero client downtime.
Endpoints
Each request carries a fresh challenge and a timestamp. Nonce and clock binding on every request; replays are refused before they touch the database.
POST https://auth.obsidian.rip/v1/auth/init
{
"application_id": "…", "environment": "production",
"challenge": "<32 random bytes, base64url>", "timestamp": 1789456123,
"license": "ATL-…", // or "bridge_token": "…"
"client_version": "1.0.0",
"fingerprint": "…", "fingerprint_version": 1,
"sdk_version": "javascript/0.1.0"
}/v1/auth/initInitialize a licensed device session. Start a session with a license key or, after a hosted sign-in, a bridge token. The answer binds this device and carries the session, the entitlements and everything else the client needs.
- application_idstring, uuid · required
- environmentdevelopment | staging | production · required
- challengestring · required
- Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
- timestampinteger · required
- Unix seconds within 30 seconds of server time.
- licensestring · optional
- A license key. Exactly one of license or bridge_token.
- client_versionstring · required
- fingerprintstring · required
- fingerprint_version1 (constant) · required
- sdk_versionstring · required
- bridge_tokenstring · optional
- A single-use, two-minute bridge token minted by POST /api/v1/hosted/bridge after a hosted sign-in, bound to that sign-in's license and to this device's fingerprint. Exactly one of license or bridge_token.
- device_proof · optional
- protection_epochinteger · optional
- The generation of client protection this build belongs to. An application that has retired older generations refuses anything below its floor with EPOCH_OUTDATED, and judges every later heartbeat against the floor in force at that moment.
Answers: 200 Signed ACTIVE session. Includes rotating token, entitlement sources, 30-second expiry and 5-second heartbeat interval. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..
/v1/auth/heartbeatRecheck current policy and rotate session token. Every five seconds. The runtime re-reads the license, the customer, the product, the application and the device as they are now, rotates the token, and answers signed. A session that misses its thirty-second deadline expires.
- application_idstring, uuid · required
- environmentdevelopment | staging | production · required
- challengestring · required
- Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
- timestampinteger · required
- Unix seconds within 30 seconds of server time.
- session_idstring, uuid · required
- tokenstring · required
Answers: 200 Signed ACTIVE session. Includes rotating token, entitlement sources, 30-second expiry and 5-second heartbeat interval. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..
/v1/auth/refreshRefresh using identical heartbeat rotation rules. The same rotation as a heartbeat, for a client that wants a fresh token outside the heartbeat cadence. The old token never rotates again.
- application_idstring, uuid · required
- environmentdevelopment | staging | production · required
- challengestring · required
- Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
- timestampinteger · required
- Unix seconds within 30 seconds of server time.
- session_idstring, uuid · required
- tokenstring · required
Answers: 200 Signed ACTIVE session. Includes rotating token, entitlement sources, 30-second expiry and 5-second heartbeat interval. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..
/v1/auth/logoutTerminate a session. Ends the session. The answer is a signed envelope whose authorization_state is TERMINATED.
- application_idstring, uuid · required
- environmentdevelopment | staging | production · required
- challengestring · required
- Unpadded canonical base64url of 16–64 cryptographically random bytes. Never reuse.
- timestampinteger · required
- Unix seconds within 30 seconds of server time.
- session_idstring, uuid · required
- tokenstring · required
Answers: 200 Signed TERMINATED acknowledgement; no token or active claims. · 400 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 403 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 409 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 429 Stable error code; fail closed. Use a fresh challenge for any subsequent request. · 503 Stable error code; fail closed. Use a fresh challenge for any subsequent request..
/v1/auth/keysDiscover current public key metadata; clients must pin trust out of band. The signing key in use and every key clients may still trust, so a rotation can be pinned ahead of time. Trust is established out of band; a client must not adopt a key it reads here.
Answers: 200 Current signing key.
What an answer carries
Beyond the session itself, the init and heartbeat payloads carry what a client needs to behave without a second request.
- session_id, token, expires_at
- The session, the token to send with the next heartbeat (it rotates every time), and when the session lapses without one. Stop protected work at
expires_atif the network is gone. - principal_id, device_id
- The holder and the bound device, as stable identifiers.
- entitlements, entitlement_sources
- Every key resolved for this license, with the layer each value came from. Entitlements resolve through six layers with one implementation, and every value says where it came from.
- release, rollout
- On init: the newest published release on the license's channel with
update_available, and a rollout policy when one applies. A required rollout below its target refuses withVERSION_BLOCKED. - configuration, configuration_revision, preferences
- Typed remote configuration in the signed answer, with server-only values that never leave the control plane. Preferences are the holder's own values under your preference schema, never an entitlement.
- seats
- For a floating license:
{max, used, available}in every accepted answer. Floating licensing with a seat budget that holds under concurrency. - heartbeat_interval, authorization_state
- Five seconds, and
ACTIVE; a logout answers a terminal payload markedTERMINATED.
Refusals
A refusal is a 403 with a code (a 400 for a malformed request, a 429 when a ceiling is reached).Denials can carry your reason, signed, so the client can show it. You can also give each code your own explanation and next step in the workspace; the portal and your software can show those instead of the platform's words.
HTTP 403
{
"error": { "code": "LICENSE_FROZEN", "message": "…", "request_id": "…" },
"statement": { "key_id": "v1", "payload": "…", "signature": "…" }
}
// the statement's payload: { protocol_version: 1, request_id, session_id, application_id,
// environment, server_timestamp, challenge, authorization_state: "DENIED", code, reason }| Code | Why | What the holder can do |
|---|---|---|
CREDENTIAL_INVALID | The license key is not one this application knows. | Check the key for typing mistakes, or redeem it in the portal. |
TOKEN_INVALID | The session token did not match the session. | Restart the application to sign in again. |
SESSION_EXPIRED | The session went too long without a heartbeat. | Restart the application to sign in again. |
SESSION_REVOKED | The session was ended because something about the license, device or account changed. | Sign in again; if it happens again, check the license in the portal. |
SESSION_TERMINATED | The session was ended on purpose, by you from the portal or by the seller. | Sign in again if it was not you. |
LICENSE_INACTIVE | The license is revoked or banned. | Nothing on their side; this is your decision. |
SEATS_EXHAUSTED | Every seat of this license is in use by a running session somewhere else. | Close the application on another machine, or wait for its session to expire, then try again. |
LICENSE_FROZEN | The license is frozen: its time is held and it cannot be used until it is unfrozen. | Unfreeze it in the portal, if the seller allows that, or contact the seller. |
LICENSE_EXPIRED | The license's time has run out. | Add time to it in the portal, or buy a new key. |
LICENSE_UNREDEEMED | This application requires a key to be claimed before it works. | Redeem the key in the portal, then sign in. |
ORGANIZATION_INACTIVE | The seller's account is not active. | Nothing on their side; this is your decision. |
APPLICATION_INACTIVE | The seller has paused this application for everyone. | Nothing on their side; this is your decision. |
APPLICATION_MAINTENANCE | The application is in maintenance: running sessions continue, new ones wait. | Nothing on their side; this is your decision. |
ENVIRONMENT_INVALID | The application asked for an environment that does not exist here. | Nothing on their side; this is your decision. |
VERSION_BLOCKED | The version of the application you are running is not allowed any more. | Update the application. |
CUSTOMER_BANNED | Your account has been banned by the seller. | Nothing on their side; this is your decision. |
ENTITLEMENT_INVALID | The product this license is for is no longer offered. | Nothing on their side; this is your decision. |
DEVICE_LIMIT_REACHED | The license is already in use on as many devices as it allows. | Reset a device in the portal, or sign in from one already registered. |
DEVICE_NOT_ENROLLED | This application requires a device key, and this machine has not enrolled one. | Run the application on this machine to enrol it, then sign in again. |
DEVICE_PROOF_INVALID | This machine could not prove it holds the key it enrolled. | Reinstall or re-enrol on this machine. A copied installation cannot sign in on another computer. |
DEVICE_TRUST_INSUFFICIENT | This machine's device key is weaker than this application accepts. | Nothing on their side; this is your decision. |
DEVICE_KEY_IN_USE | That device key is already enrolled to another machine on this licence. | Nothing on their side; this is your decision. |
EPOCH_OUTDATED | This build belongs to a generation of client protection the seller has retired. | Update to the current build. |
LEASE_UNAVAILABLE | This application does not issue offline leases. | Nothing on their side; this is your decision. |
LEASE_REQUIRES_DEVICE_KEY | An offline lease is bound to this machine's device key, and none is enrolled. | Enrol a device key, then request the lease again. |
DEVICE_BLOCKED | This device has been blocked by the seller. | Nothing on their side; this is your decision. |
RISK_DENIED | The seller's policy denies this sign-in. | Nothing on their side; this is your decision. |
RATE_LIMITED | Too many requests in a short time. | Wait a minute and try again. |
CHALLENGE_STALE | The device's clock is more than thirty seconds off. | Set the device's clock correctly and try again. |
CHALLENGE_REUSED | A request was repeated with the same one-time challenge. | Restart the application; if it persists, update it. |
BRIDGE_INVALID | The sign-in hand-off to the application is unknown or has expired. | Sign in again in the application. |
BRIDGE_REPLAYED | A sign-in hand-off was used a second time; the session it started has been ended. | Sign in again in the application. If you did not do this, change your password. |
BRIDGE_MISMATCH | The sign-in hand-off was for a different device. | Sign in again on this device. |
BRIDGE_REVOKED | The hosted sign-in behind the hand-off was signed out. | Sign in again in the application. |
The SDK adds its own refusals when an answer fails verification: INVALID_ENVELOPE, UNTRUSTED_KEY, SIGNATURE_INVALID, RESPONSE_BINDING_INVALID, SESSION_BINDING_INVALID and SESSION_INVALID. None of these came from the runtime, and none carries a reason.
The event stream
Live, signed notifications to running clients. The heartbeat stays the authority.
GET /v1/auth/stream opens a server-sent event stream bound to one session, authorized with the same proof a heartbeat carries. It begins with a retry hint and a hello frame, then delivers session.ended (the session was terminated, the license revoked or frozen, the device blocked or reset, the customer banned) and config.changed (something about the license, its entitlements, the product, configuration, releases or announcements changed) as they happen, and ends with bye when a second stream replaces it or the client falls behind.
GET https://auth.obsidian.rip/v1/auth/stream
Authorization: Bearer <the current session token>
X-Obsidian-Session: <session id>
X-Obsidian-Application: <application id>
X-Obsidian-Environment: production
retry: 3200
event: hello
data: {"key_id":"v1","payload":"…","signature":"…"}
event: session.ended
data: {"key_id":"v1","payload":"…","signature":"…"} // payload.action, .reason, .sequenceEvery frame's data is a signed envelope, verified and bound to the session before its type is read. A push carries no claim: on session.ended the client asks the heartbeat, and it is the heartbeat's refusal that clears the session; on config.changed nothing changes until you re-read what you cached. A severed stream changes nothing either; the session expires at the deadline it already had. The JavaScript SDK's subscribe does all of this; see the SDK examples.