feat: edge gateway service
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
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=<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`.
|
||||
|
||||
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: []
|
||||
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 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
|
||||
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:
|
||||
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
|
||||
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.
|
||||
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
|
||||
Reference in New Issue
Block a user