20 KiB
Orama Vault -- API Reference
Base URL
All endpoints are prefixed with /v1/vault/ (V1) or /v2/vault/ (V2). The guardian listens on the configured client port (default: 7500).
http://<guardian-ip>:7500/v1/vault/...
http://<guardian-ip>:7500/v2/vault/...
In production, the Orama gateway reverse-proxies these endpoints over TLS (port 443). Direct access to port 7500 is only available within the WireGuard overlay network.
Note: TLS termination is not yet implemented in the guardian itself (Phase 3). Currently plain TCP.
V1 Endpoints
GET /v1/vault/health
Liveness check. Returns immediately with a static response. No authentication required. Used by load balancers and monitoring.
Request:
GET /v1/vault/health HTTP/1.1
Response (200 OK):
{
"status": "ok",
"version": "0.1.0"
}
This endpoint never returns an error. If the process is running and the TCP listener is accepting connections, it returns 200.
GET /v1/vault/status
Guardian status information. Returns configuration and runtime state. No authentication required.
Request:
GET /v1/vault/status HTTP/1.1
Response (200 OK):
{
"status": "ok",
"version": "0.1.0",
"data_dir": "/opt/orama/.orama/data/vault",
"client_port": 7500,
"peer_port": 7501
}
GET /v1/vault/guardians
List known guardian nodes. In the current MVP, returns only the local node. Phase 3 will query RQLite for the full cluster list.
Request:
GET /v1/vault/guardians HTTP/1.1
Response (200 OK):
{
"guardians": [
{
"address": "0.0.0.0",
"port": 7500
}
],
"threshold": 3,
"total": 1
}
| Field | Type | Description |
|---|---|---|
guardians |
array | List of known guardian nodes |
guardians[].address |
string | Node IP address |
guardians[].port |
number | Node client port |
threshold |
number | Shamir threshold K (minimum shares to reconstruct) |
total |
number | Total known guardians |
POST /v1/vault/push
Store an encrypted share for a user. The client has already performed the Shamir split locally and sends one share to each guardian.
Request:
POST /v1/vault/push HTTP/1.1
Content-Type: application/json
Content-Length: <length>
{
"identity": "<64 hex chars>",
"share": "<base64-encoded share data>",
"version": <uint64>
}
| Field | Type | Required | Constraints |
|---|---|---|---|
identity |
string | yes | Exactly 64 lowercase hex characters (SHA-256 of user identity) |
share |
string | yes | Base64-encoded encrypted share data. Decoded size must be > 0 and <= 512 KiB |
version |
number | yes | Unsigned 64-bit integer. Must be strictly greater than the currently stored version (monotonic counter) |
Success Response (200 OK):
{
"status": "stored"
}
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | {"error":"empty body"} |
Request body is empty |
| 400 | {"error":"request body too large"} |
Body exceeds 1 MiB |
| 400 | {"error":"missing identity field"} |
identity field not found in JSON |
| 400 | {"error":"missing share field"} |
share field not found in JSON |
| 400 | {"error":"identity must be exactly 64 hex characters"} |
Identity is not 64 chars |
| 400 | {"error":"identity must be hex"} |
Identity contains non-hex characters |
| 400 | {"error":"invalid base64 in share"} |
Share data is not valid base64 |
| 400 | {"error":"share data too large"} |
Decoded share exceeds 512 KiB |
| 400 | {"error":"share data is empty"} |
Decoded share is 0 bytes |
| 400 | {"error":"missing or invalid version field"} |
version field missing or not a valid unsigned integer |
| 400 | {"error":"version must be greater than current stored version"} |
Anti-rollback: version <= stored version |
| 405 | {"error":"method not allowed"} |
Non-POST method used |
| 500 | {"error":"internal server error"} |
Disk write failure or allocation error |
Storage Behavior:
- Share data is written atomically: first to
share.bin.tmp, then renamed toshare.bin. - Version counter is written atomically: first to
version.tmp, then renamed toversion. - Anti-rollback: if a version file exists for this identity, the new version must be strictly greater. Equal versions are also rejected.
- Directory path:
<data_dir>/shares/<identity>/share.bin
Size Limits:
| Limit | Value |
|---|---|
| Max request body | 1 MiB (1,048,576 bytes) |
| Max decoded share | 512 KiB (524,288 bytes) |
| Identity length | Exactly 64 hex characters |
| Max version value | 2^64 - 1 |
POST /v1/vault/pull
Retrieve an encrypted share for a user. The client contacts multiple guardians to collect K shares for reconstruction.
Request:
POST /v1/vault/pull HTTP/1.1
Content-Type: application/json
Content-Length: <length>
{
"identity": "<64 hex chars>"
}
| Field | Type | Required | Constraints |
|---|---|---|---|
identity |
string | yes | Exactly 64 lowercase hex characters |
Success Response (200 OK):
{
"share": "<base64-encoded share data>"
}
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | {"error":"empty body"} |
Request body is empty |
| 400 | {"error":"request body too large"} |
Body exceeds 4 KiB |
| 400 | {"error":"missing identity field"} |
identity not found |
| 400 | {"error":"identity must be exactly 64 hex characters"} |
Wrong length |
| 400 | {"error":"identity must be hex"} |
Non-hex characters |
| 404 | {"error":"share not found"} |
No share stored for this identity |
| 405 | {"error":"method not allowed"} |
Non-POST method used |
| 500 | {"error":"internal server error"} |
Disk read failure |
Size Limits:
| Limit | Value |
|---|---|
| Max request body | 4 KiB (4,096 bytes) |
| Max share read | 1 MiB (1,048,576 bytes) |
POST /v1/vault/auth/challenge
Request a challenge nonce to begin authentication.
Request:
POST /v1/vault/auth/challenge HTTP/1.1
Content-Type: application/json
{
"identity": "<64 hex chars>"
}
Response (200 OK):
{
"nonce": "<base64 32 bytes>",
"created_ns": <i128>,
"tag": "<base64 32 bytes>",
"expires_in_seconds": 60
}
The client must return this exact challenge (nonce + created_ns + tag) along with their identity within 60 seconds.
POST /v1/vault/auth/session
Exchange a verified challenge for a session token.
Request:
POST /v1/vault/auth/session HTTP/1.1
Content-Type: application/json
{
"identity": "<64 hex chars>",
"nonce": "<base64 32 bytes>",
"created_ns": <i128>,
"tag": "<base64 32 bytes>"
}
Response (200 OK):
{
"session_token": "<base64-encoded token>",
"expires_in_seconds": 3600
}
The session token is valid for 1 hour. It should be included in subsequent requests as a Bearer token in the Authorization header.
V1 Authentication Flow
The authentication flow is challenge-response:
Client Guardian
| |
| POST /v1/vault/auth/challenge |
| {"identity":"<hex>"} |
|----------------------------------->|
| | Generate 32-byte random nonce
| | HMAC(server_secret, identity || nonce || timestamp)
| {"nonce":"..","tag":".."} |
|<-----------------------------------|
| |
| POST /v1/vault/auth/session |
| {"identity":"..","nonce":"..","tag":".."}
|----------------------------------->|
| | Verify HMAC tag
| | Check nonce not expired (60s)
| {"session_token":".."} | Issue HMAC-based session token (1h)
|<-----------------------------------|
| |
| POST /v1/vault/push |
| Authorization: Bearer <token> |
|----------------------------------->|
| | Verify session token
| | Process request
Key properties:
- Challenge expires in 60 seconds.
- Session tokens expire in 1 hour.
- All HMAC verifications use constant-time comparison to prevent timing attacks.
- Server secret is generated randomly at startup (not persisted -- sessions invalidate on restart).
- Phase 3 adds Ed25519 signature verification for true public-key authentication.
V2 Endpoints
V2 introduces a generic secrets API. Instead of storing a single anonymous share per identity, V2 allows multiple named secrets per identity with full CRUD operations.
All V2 secrets endpoints require mandatory session authentication via the X-Session-Token header. The identity is extracted from the session token -- it is never passed in the request body. Authenticate first using the V2 auth endpoints below.
POST /v2/vault/auth/challenge
Request a challenge nonce to begin authentication. Same protocol as V1 auth/challenge.
Request:
POST /v2/vault/auth/challenge HTTP/1.1
Content-Type: application/json
{
"identity": "<64 hex chars>"
}
Response (200 OK):
{
"nonce": "<base64 32 bytes>",
"created_ns": <i128>,
"tag": "<base64 32 bytes>",
"expires_in_seconds": 60
}
POST /v2/vault/auth/session
Exchange a verified challenge for a session token. Same protocol as V1 auth/session.
Request:
POST /v2/vault/auth/session HTTP/1.1
Content-Type: application/json
{
"identity": "<64 hex chars>",
"nonce": "<base64 32 bytes>",
"created_ns": <i128>,
"tag": "<base64 32 bytes>"
}
Response (200 OK):
{
"session_token": "<base64-encoded token>",
"expires_in_seconds": 3600
}
The session token is valid for 1 hour. Include it in all subsequent V2 requests as the X-Session-Token header.
PUT /v2/vault/secrets/{name}
Store a named secret. Requires session authentication. The identity is extracted from the session token.
Request:
PUT /v2/vault/secrets/my-api-key HTTP/1.1
Content-Type: application/json
X-Session-Token: <session_token>
{
"share": "<base64-encoded secret data>",
"version": <u64>
}
| Field | Type | Required | Constraints |
|---|---|---|---|
name (URL path) |
string | yes | Alphanumeric, _, -. Max 128 characters |
share |
string | yes | Base64-encoded data. Decoded size must be > 0 and <= 512 KiB |
version |
number | yes | Unsigned 64-bit integer. Must be strictly greater than the currently stored version (anti-rollback) |
Success Response (200 OK):
{
"status": "stored",
"name": "my-api-key",
"version": 1
}
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 400 | {"error":"empty body"} |
Request body is empty |
| 400 | {"error":"invalid secret name"} |
Name contains disallowed characters or exceeds 128 chars |
| 400 | {"error":"missing share field"} |
share not found in JSON |
| 400 | {"error":"invalid base64 in share"} |
Share is not valid base64 |
| 400 | {"error":"share data too large"} |
Decoded share exceeds 512 KiB |
| 400 | {"error":"share data is empty"} |
Decoded share is 0 bytes |
| 400 | {"error":"missing or invalid version field"} |
version missing or invalid |
| 400 | {"error":"version must be greater than current stored version"} |
Anti-rollback: version <= stored version |
| 401 | {"error":"missing session token"} |
X-Session-Token header not provided |
| 401 | {"error":"invalid session token"} |
Token is malformed or expired |
| 409 | {"error":"too many secrets"} |
Identity has reached the 1000 secret limit |
| 500 | {"error":"internal server error"} |
Disk write failure |
Storage Layout:
<data_dir>/vaults/<identity_hex>/<secret_name>/
share.bin -- Encrypted share data
checksum.bin -- HMAC-SHA256 integrity checksum
meta.json -- {"version":1,"created_ns":...,"updated_ns":...,"size":123}
Limits:
| Limit | Value |
|---|---|
| Max secrets per identity | 1000 |
| Max decoded share size | 512 KiB (524,288 bytes) |
| Max secret name length | 128 characters |
| Secret name charset | [a-zA-Z0-9_-] |
| Max version value | 2^64 - 1 |
GET /v2/vault/secrets/{name}
Retrieve a named secret. Requires session authentication. The identity is extracted from the session token.
Request:
GET /v2/vault/secrets/my-api-key HTTP/1.1
X-Session-Token: <session_token>
Success Response (200 OK):
{
"share": "<base64-encoded secret data>",
"name": "my-api-key",
"version": 1,
"created_ns": 1700000000000000000,
"updated_ns": 1700000000000000000
}
| Field | Type | Description |
|---|---|---|
share |
string | Base64-encoded secret data |
name |
string | Secret name |
version |
number | Current version |
created_ns |
number | Creation timestamp in nanoseconds |
updated_ns |
number | Last update timestamp in nanoseconds |
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | {"error":"missing session token"} |
X-Session-Token header not provided |
| 401 | {"error":"invalid session token"} |
Token is malformed or expired |
| 404 | {"error":"secret not found"} |
No secret with this name for this identity |
| 500 | {"error":"internal server error"} |
Disk read failure |
DELETE /v2/vault/secrets/{name}
Delete a named secret. Requires session authentication. The identity is extracted from the session token.
Request:
DELETE /v2/vault/secrets/my-api-key HTTP/1.1
X-Session-Token: <session_token>
Success Response (200 OK):
{
"status": "deleted",
"name": "my-api-key"
}
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | {"error":"missing session token"} |
X-Session-Token header not provided |
| 401 | {"error":"invalid session token"} |
Token is malformed or expired |
| 404 | {"error":"secret not found"} |
No secret with this name for this identity |
| 500 | {"error":"internal server error"} |
Disk delete failure |
GET /v2/vault/secrets
List all secrets for the authenticated identity. Requires session authentication. The identity is extracted from the session token.
Request:
GET /v2/vault/secrets HTTP/1.1
X-Session-Token: <session_token>
Success Response (200 OK):
{
"secrets": [
{
"name": "my-api-key",
"version": 1,
"size": 256
},
{
"name": "db-password",
"version": 3,
"size": 48
}
]
}
| Field | Type | Description |
|---|---|---|
secrets |
array | List of secret metadata entries |
secrets[].name |
string | Secret name |
secrets[].version |
number | Current version |
secrets[].size |
number | Size of stored data in bytes |
Error Responses:
| Status | Body | Condition |
|---|---|---|
| 401 | {"error":"missing session token"} |
X-Session-Token header not provided |
| 401 | {"error":"invalid session token"} |
Token is malformed or expired |
| 500 | {"error":"internal server error"} |
Disk read failure |
If no secrets exist for the identity, returns an empty array: {"secrets":[]}.
V2 Authentication Flow
The V2 authentication flow is identical to V1 but uses V2 path prefixes and the X-Session-Token header:
Client Guardian
| |
| POST /v2/vault/auth/challenge |
| {"identity":"<hex>"} |
|----------------------------------->|
| | Generate 32-byte random nonce
| | HMAC(server_secret, identity || nonce || timestamp)
| {"nonce":"..","tag":".."} |
|<-----------------------------------|
| |
| POST /v2/vault/auth/session |
| {"identity":"..","nonce":"..","tag":".."}
|----------------------------------->|
| | Verify HMAC tag
| | Check nonce not expired (60s)
| {"session_token":".."} | Issue HMAC-based session token (1h)
|<-----------------------------------|
| |
| PUT /v2/vault/secrets/my-key |
| X-Session-Token: <token> |
| {"share":"..","version":1} |
|----------------------------------->|
| | Verify session token
| | Extract identity from token
| {"status":"stored",...} | Store secret under identity
|<-----------------------------------|
Key differences from V1:
- Session token is sent via
X-Session-Tokenheader (not Authorization Bearer). - Identity is extracted from the session token, not from the request body.
- All V2 secrets endpoints require authentication (mandatory, not optional).
Error Response Format
All error responses use a consistent JSON format:
{
"error": "<human-readable error message>"
}
Standard HTTP status codes:
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Client error (bad request, validation failure) |
| 401 | Authentication required or token invalid |
| 404 | Resource not found (share/secret not found, unknown endpoint) |
| 405 | Method not allowed |
| 409 | Conflict (e.g., too many secrets) |
| 500 | Internal server error |
All responses include Connection: close and Content-Type: application/json headers.
Rate Limiting (not yet implemented)
Status: Rate limiting is planned for Phase 3. Currently there are no request rate limits.
Planned behavior:
- Per-identity rate limiting on push/pull endpoints.
- Health and status endpoints exempt from rate limiting.
- Rate limit headers in responses (
X-RateLimit-Limit,X-RateLimit-Remaining).
Request Size Limits
| Endpoint | Max Body Size |
|---|---|
/v1/vault/push |
1 MiB |
/v1/vault/pull |
4 KiB |
/v2/vault/secrets/{name} (PUT) |
1 MiB |
| All others | 64 KiB (read buffer size) |
The HTTP listener uses a 64 KiB read buffer. Requests larger than this may be truncated. The push handler and V2 PUT handler have an explicit 1 MiB limit check before processing.
Peer Protocol (Port 7501)
The guardian-to-guardian protocol is a binary TCP protocol, not HTTP. It runs on port 7501 and is restricted to the WireGuard overlay network (10.0.0.x).
Wire Format
[version:1 byte][msg_type:1 byte][payload_length:4 bytes big-endian][payload:N bytes]
- version: Protocol version (currently 1). Messages with wrong version are silently dropped.
- msg_type: One of the defined message types.
- payload_length: 32-bit big-endian unsigned integer. Maximum 1 MiB.
Message Types
| Code | Name | Direction | Payload Size |
|---|---|---|---|
| 0x01 | heartbeat | initiator -> peer | 18 bytes |
| 0x02 | heartbeat_ack | peer -> initiator | 0 bytes |
| 0x03 | verify_request | initiator -> peer | 65 bytes |
| 0x04 | verify_response | peer -> initiator | 98 bytes |
| 0x05 | repair_offer | leader -> all | (Phase 2) |
| 0x06 | repair_accept | peer -> leader | (Phase 2) |
Heartbeat Payload (18 bytes)
[sender_ip:4 bytes][sender_port:2 bytes BE][share_count:4 bytes BE][timestamp:8 bytes BE]
Verify Request Payload (65 bytes)
[identity:64 bytes][identity_len:1 byte]
Verify Response Payload (98 bytes)
[identity:64 bytes][identity_len:1 byte][has_share:1 byte (0/1)][commitment_root:32 bytes SHA-256]