openapi: 3.1.0 info: title: Shade Prekey Server description: | Signal Protocol prekey distribution server. Stores identity public keys and prekey bundles. Any language can implement a client — this spec documents the wire contract. **Security model:** Write operations (register, replenish, delete) are authenticated by Ed25519 signatures over the request body. Bundle fetches are anonymous. See the `SignedPayload` schema for the signing format. version: "1.0.0" license: name: MIT url: https://gt.zyon.no/Stian/Shade/raw/branch/main/LICENSE servers: - url: https://shade.example.com description: Replace with your project's prekey server URL paths: /health: get: summary: Health check description: Returns 200 if the server and its storage backend are reachable. tags: [Infrastructure] responses: '200': description: Healthy content: application/json: schema: $ref: '#/components/schemas/HealthResponse' '503': description: Unhealthy /metrics: get: summary: Prometheus metrics description: Counter, histogram, and gauge metrics in Prometheus text format. tags: [Infrastructure] responses: '200': description: Metrics content: text/plain: schema: type: string example: | # HELP shade_requests_total Total HTTP requests # TYPE shade_requests_total counter shade_requests_total{route="/v1/keys/register",status="200"} 42 /openapi.yaml: get: summary: This OpenAPI spec description: Serves the YAML spec itself, so clients can auto-download it. tags: [Infrastructure] responses: '200': description: OpenAPI spec content: application/yaml: schema: type: string /v1/keys/register: post: summary: Register identity and upload prekey bundle description: | Register a new identity with the prekey server, or update an existing one. The request body must be signed with the identity's own Ed25519 signing key (TOFU — first signature establishes the identity). tags: [Keys] security: - Ed25519Signature: [] requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/SignedPayload' - type: object required: [address, identitySigningKey, identityDHKey, signedPreKey] properties: address: $ref: '#/components/schemas/Address' identitySigningKey: type: string description: Ed25519 public key, base64 identityDHKey: type: string description: X25519 public key, base64 signedPreKey: $ref: '#/components/schemas/SignedPreKeyEntry' oneTimePreKeys: type: array items: $ref: '#/components/schemas/OneTimePreKeyEntry' responses: '200': description: Registered content: application/json: schema: type: object properties: ok: { type: boolean } '400': $ref: '#/components/responses/ValidationError' '401': $ref: '#/components/responses/Unauthorized' '429': $ref: '#/components/responses/RateLimited' /v1/keys/bundle/{address}: get: summary: Fetch a prekey bundle (anonymous) description: | Fetch an identity's prekey bundle so Alice can start an X3DH session with Bob. Each call consumes one one-time prekey (FIFO) if any are available — if not, returns a bundle without the one-time prekey. tags: [Keys] parameters: - name: address in: path required: true schema: $ref: '#/components/schemas/Address' responses: '200': description: Prekey bundle content: application/json: schema: $ref: '#/components/schemas/PreKeyBundle' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/RateLimited' /v1/keys/count/{address}: get: summary: Get remaining one-time prekey count (anonymous) tags: [Keys] parameters: - name: address in: path required: true schema: $ref: '#/components/schemas/Address' responses: '200': description: Count content: application/json: schema: type: object properties: count: { type: integer, minimum: 0 } /v1/keys/replenish: post: summary: Upload more one-time prekeys (signed) tags: [Keys] security: - Ed25519Signature: [] requestBody: required: true content: application/json: schema: allOf: - $ref: '#/components/schemas/SignedPayload' - type: object required: [address, oneTimePreKeys] properties: address: $ref: '#/components/schemas/Address' oneTimePreKeys: type: array items: $ref: '#/components/schemas/OneTimePreKeyEntry' responses: '200': description: Replenished content: application/json: schema: type: object properties: ok: { type: boolean } remaining: { type: integer, minimum: 0 } '401': $ref: '#/components/responses/Unauthorized' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/RateLimited' /v1/keys/{address}: delete: summary: Unregister an identity (signed) tags: [Keys] security: - Ed25519Signature: [] parameters: - name: address in: path required: true schema: $ref: '#/components/schemas/Address' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SignedPayload' responses: '200': description: Deleted '401': $ref: '#/components/responses/Unauthorized' /shade-observer/api/state: get: summary: Current observer snapshot (optional) description: | Returns the aggregated state visible to the dashboard. Only available if the server was started with `SHADE_OBSERVER_TOKEN` set. tags: [Observer] security: - ObserverBearerToken: [] responses: '200': description: Observer state snapshot '401': description: Invalid or missing token components: schemas: Address: type: string description: | An address on the prekey server. Alphanumeric plus `:_-.` characters, max 256 chars. NFKC-normalized. Format is typically `user@domain:deviceId` or `device:uuid`. pattern: '^[a-zA-Z0-9][a-zA-Z0-9:_\-.]{0,255}$' example: "alice@example.com:phone" SignedPayload: type: object description: | Base type for all signed request bodies. The `signature` field is an Ed25519 signature over the canonical JSON of the payload (all fields sorted, `signature` omitted) using the identity's signing private key. `signedAt` must be within ±5 minutes of server time. required: [signedAt, signature] properties: signedAt: type: integer format: int64 description: Unix epoch milliseconds signature: type: string description: Ed25519 signature, base64 SignedPreKeyEntry: type: object required: [keyId, publicKey, signature] properties: keyId: type: integer minimum: 0 publicKey: type: string description: X25519 public key, base64 signature: type: string description: Ed25519 signature over `publicKey`, base64 OneTimePreKeyEntry: type: object required: [keyId, publicKey] properties: keyId: type: integer minimum: 0 publicKey: type: string description: X25519 public key, base64 PreKeyBundle: type: object required: [identitySigningKey, identityDHKey, signedPreKey] properties: identitySigningKey: type: string description: Ed25519 public key, base64 identityDHKey: type: string description: X25519 public key, base64 signedPreKey: $ref: '#/components/schemas/SignedPreKeyEntry' oneTimePreKey: $ref: '#/components/schemas/OneTimePreKeyEntry' HealthResponse: type: object properties: status: type: string enum: [ok, error] service: type: string ErrorResponse: type: object properties: name: type: string code: type: string pattern: '^SHADE_' message: type: string responses: ValidationError: description: Invalid request body content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' Unauthorized: description: Signature verification failed or replay window exceeded content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' NotFound: description: Address not registered content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' RateLimited: description: Rate limit exceeded headers: Retry-After: schema: type: integer description: Seconds until the client can retry content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' securitySchemes: Ed25519Signature: type: apiKey in: header name: X-Shade-Signature-Info description: | NOT an actual header. Signature is carried inside the JSON request body as the `signature` and `signedAt` fields (see SignedPayload). The canonical form signed is the request body with all keys sorted and the `signature` field omitted. Use your Ed25519 signing key. ObserverBearerToken: type: http scheme: bearer description: | `Authorization: Bearer `. The observer also accepts the token via `?token=...` query string for SSE endpoints that can't set headers.