openapi: 3.0.3 info: title: Galaxy Edge Gateway Public REST API version: v1 description: | This specification documents the implemented `galaxy/gateway` v1 public REST surface. Implemented endpoints: - `GET /healthz` - `GET /readyz` - `POST /api/v1/public/auth/send-email-code` - `POST /api/v1/public/auth/confirm-email-code` This specification intentionally excludes the private operational admin listener and its `GET /metrics` endpoint. That endpoint is documented in `README.md` because it is not part of the public REST contract. Common runtime behavior: - requests are unauthenticated; - unknown routes return `404` with the JSON error envelope; - unsupported methods on implemented routes and browser-shaped public paths return `405` with the same JSON error envelope and an `Allow` header; - request classification happens before route handling and depends on the incoming method, path, and selected headers; - the only stable public route classes are `public_auth`, `browser_bootstrap`, `browser_asset`, and `public_misc`; - any unsupported or empty classifier result is normalized to `public_misc`; - public REST policy derives its base bucket namespace from the normalized class as `public_rest/class=`; - per-IP public REST rate limits use only `RemoteAddr`; `X-Forwarded-For` and `Forwarded` are intentionally ignored; - `public_auth` additionally applies normalized identity buckets by `email` for `send-email-code` and by `challenge_id` for `confirm-email-code`; - oversized request bodies are rejected with `413 request_too_large`; - public REST rate limits reject with `429 rate_limited` and a `Retry-After` header; - public auth routes delegate through `AuthServiceClient`; - the default `cmd/gateway` wiring keeps the auth routes mounted and returns `503 service_unavailable` until a concrete upstream auth adapter is configured; - injected public auth adapters may also project client-safe `4xx/5xx` `AuthServiceError` envelopes, which the gateway preserves after normalizing blank or invalid fields. servers: - url: http://localhost:8080 description: | Example local public REST listener. The actual address is configured by `GATEWAY_PUBLIC_HTTP_ADDR`. tags: - name: Probes description: Unauthenticated public probe endpoints served by the gateway. - name: PublicAuth description: | Unauthenticated public auth endpoints delegated to the Auth / Session Service through `AuthServiceClient`. paths: /healthz: get: tags: - Probes operationId: getHealthz summary: Public liveness probe description: | Returns a deterministic JSON payload confirming that the public REST listener is alive and able to answer requests. security: [] x-public-route-classification-note: | Typical probe requests are classified as `public_misc`. Requests that match browser bootstrap rules, for example because they advertise `Accept: text/html`, are classified as `browser_bootstrap` before the route handler runs. responses: "200": description: Public REST listener is alive. content: application/json: schema: $ref: "#/components/schemas/HealthzResponse" examples: ok: value: status: ok "413": $ref: "#/components/responses/RequestTooLargeError" "429": $ref: "#/components/responses/RateLimitedError" "500": $ref: "#/components/responses/InternalError" /readyz: get: tags: - Probes operationId: getReadyz summary: Public readiness probe description: | Returns a deterministic JSON payload confirming that the process is ready to accept public REST traffic. Readiness is local-process only and does not reflect downstream dependencies. security: [] x-public-route-classification-note: | Typical probe requests are classified as `public_misc`. Requests that match browser bootstrap rules, for example because they advertise `Accept: text/html`, are classified as `browser_bootstrap` before the route handler runs. responses: "200": description: Public REST listener is ready to accept traffic. content: application/json: schema: $ref: "#/components/schemas/ReadyzResponse" examples: ready: value: status: ready "413": $ref: "#/components/responses/RequestTooLargeError" "429": $ref: "#/components/responses/RateLimitedError" "500": $ref: "#/components/responses/InternalError" /api/v1/public/auth/send-email-code: post: tags: - PublicAuth operationId: sendEmailCode summary: Start a public e-mail login challenge description: | Accepts a single client e-mail address and delegates the command to the Auth / Session Service. The response returns an opaque `challenge_id` that must later be confirmed through `POST /api/v1/public/auth/confirm-email-code`. The JSON body stays unchanged. Callers may additionally supply the standard `Accept-Language` header so the gateway can derive the auth-mail locale and first-login preferred-language candidate. Missing or unsupported values fall back to `en`. This route is unauthenticated and classified as `public_auth`. Public REST anti-abuse applies a per-IP bucket derived from `RemoteAddr` and an additional normalized identity bucket derived from `email`. In the default `cmd/gateway` process wiring the upstream auth adapter is intentionally absent, so this route returns `503 service_unavailable` until a concrete `AuthServiceClient` is injected. When an injected adapter returns a client-safe `AuthServiceError`, the gateway preserves that projected `4xx/5xx` status and serialized error envelope after normalizing blank or invalid fields. security: [] parameters: - $ref: "#/components/parameters/AcceptLanguage" x-public-route-classification-note: | This route is always classified as `public_auth`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/SendEmailCodeRequest" examples: default: value: email: pilot@example.com responses: "200": description: The login challenge was accepted by the Auth / Session Service. content: application/json: schema: $ref: "#/components/schemas/SendEmailCodeResponse" examples: accepted: value: challenge_id: challenge-123 "400": $ref: "#/components/responses/InvalidRequestError" "413": $ref: "#/components/responses/RequestTooLargeError" "405": $ref: "#/components/responses/MethodNotAllowedError" "429": $ref: "#/components/responses/RateLimitedError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" default: $ref: "#/components/responses/ProjectedAuthServiceError" /api/v1/public/auth/confirm-email-code: post: tags: - PublicAuth operationId: confirmEmailCode summary: Confirm a public e-mail login challenge description: | Completes a previously issued `challenge_id`, sends the verification `code`, and registers the standard base64-encoded raw 32-byte Ed25519 `client_public_key` for the new device session. The caller must also supply the client-selected IANA `time_zone`, which the gateway forwards unchanged to the Auth / Session Service. The response returns the created `device_session_id`. This route is unauthenticated and classified as `public_auth`. Public REST anti-abuse applies a per-IP bucket derived from `RemoteAddr` and an additional normalized identity bucket derived from `challenge_id`. In the default `cmd/gateway` process wiring the upstream auth adapter is intentionally absent, so this route returns `503 service_unavailable` until a concrete `AuthServiceClient` is injected. When an injected adapter returns a client-safe `AuthServiceError`, the gateway preserves that projected `4xx/5xx` status and serialized error envelope after normalizing blank or invalid fields. security: [] x-public-route-classification-note: | This route is always classified as `public_auth`. requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ConfirmEmailCodeRequest" examples: default: value: challenge_id: challenge-123 code: "123456" client_public_key: base64-encoded-raw-ed25519-public-key time_zone: Europe/Kaliningrad responses: "200": description: The device session was created by the Auth / Session Service. content: application/json: schema: $ref: "#/components/schemas/ConfirmEmailCodeResponse" examples: accepted: value: device_session_id: device-session-123 "400": $ref: "#/components/responses/InvalidRequestError" "413": $ref: "#/components/responses/RequestTooLargeError" "405": $ref: "#/components/responses/MethodNotAllowedError" "429": $ref: "#/components/responses/RateLimitedError" "500": $ref: "#/components/responses/InternalError" "503": $ref: "#/components/responses/ServiceUnavailableError" default: $ref: "#/components/responses/ProjectedAuthServiceError" components: parameters: AcceptLanguage: name: Accept-Language in: header required: false description: | Optional RFC 9110 `Accept-Language` header used by gateway to derive the auth-mail locale and first-login preferred-language candidate. The first supported BCP 47 tag wins; unsupported or missing values fall back to `en`. schema: type: string schemas: HealthzResponse: type: object additionalProperties: false required: - status properties: status: type: string description: Deterministic liveness marker. enum: - ok ReadyzResponse: type: object additionalProperties: false required: - status properties: status: type: string description: Deterministic readiness marker. enum: - ready SendEmailCodeRequest: type: object additionalProperties: false required: - email properties: email: type: string description: Single client e-mail address that should receive the login code. format: email SendEmailCodeResponse: type: object additionalProperties: false required: - challenge_id properties: challenge_id: type: string description: Opaque challenge identifier returned by the Auth / Session Service. ConfirmEmailCodeRequest: type: object additionalProperties: false required: - challenge_id - code - client_public_key - time_zone properties: challenge_id: type: string description: Opaque challenge identifier previously returned by send-email-code. code: type: string description: Verification code delivered to the client. client_public_key: type: string description: Standard base64-encoded raw 32-byte Ed25519 public key registered for the new device session. time_zone: type: string description: | Client-selected IANA time zone name forwarded to the Auth / Session Service as registration context for first-time user creation. ConfirmEmailCodeResponse: type: object additionalProperties: false required: - device_session_id properties: device_session_id: type: string description: Stable identifier of the created device session. ErrorResponse: type: object additionalProperties: false required: - error properties: error: $ref: "#/components/schemas/ErrorBody" ErrorBody: type: object additionalProperties: false required: - code - message properties: code: type: string description: | Stable gateway-generated or client-safe auth-adapter-projected error code. Gateway-generated values include `invalid_request`, `not_found`, `method_not_allowed`, `request_too_large`, `rate_limited`, `internal_error`, and `service_unavailable`. message: type: string description: Human-readable client-safe error description. headers: Allow: description: Comma-separated list of allowed methods for the target route. schema: type: string example: GET Retry-After: description: Seconds until the client should retry a rejected rate-limited request. schema: type: string example: "3600" responses: InvalidRequestError: description: Request body or field values are invalid for the target public auth route. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: invalidRequest: value: error: code: invalid_request message: email must be a single valid email address NotFoundError: description: Request path is not implemented on the current public REST surface. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: notFound: value: error: code: not_found message: resource was not found MethodNotAllowedError: description: Request method is not allowed for an implemented route. headers: Allow: $ref: "#/components/headers/Allow" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: methodNotAllowed: value: error: code: method_not_allowed message: request method is not allowed for this route RequestTooLargeError: description: Request body exceeds the configured public REST body limit for the route class. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: requestTooLarge: value: error: code: request_too_large message: request body exceeds the configured limit RateLimitedError: description: Request is rejected by the public REST anti-abuse rate limiter. headers: Retry-After: $ref: "#/components/headers/Retry-After" content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: rateLimited: value: error: code: rate_limited message: request rate limit exceeded InternalError: description: Internal gateway error while processing the request. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: internalError: value: error: code: internal_error message: internal server error ServiceUnavailableError: description: | The public route is mounted, but the configured or default auth adapter cannot currently serve the request. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: unavailable: value: error: code: service_unavailable message: auth service is unavailable ProjectedAuthServiceError: description: | Client-safe `4xx/5xx` error envelope projected by an injected public auth adapter through `AuthServiceError`. The gateway preserves the projected status and serialized envelope after normalizing blank or invalid fields. content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" examples: projectedRateLimit: value: error: code: upstream_rate_limited message: too many attempts for this email