docs(api): add OpenAPI 3.1 spec for the HTTP server
This commit is contained in:
449
openapi.yaml
Normal file
449
openapi.yaml
Normal file
@@ -0,0 +1,449 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: Remanso Jetstream API
|
||||
version: 0.1.0
|
||||
description: |
|
||||
HTTP API for Remanso, a blogging platform on the AT Protocol. Exposes
|
||||
paginated read access to `space.remanso.note` records, full-text fuzzy
|
||||
search backed by OpenSearch, and per-user webhook subscriptions that fire
|
||||
on note create/update/delete.
|
||||
|
||||
Authentication uses AT Protocol PDS-issued bearer JWTs. The token's `sub`
|
||||
must equal the path `:did` for owner-scoped endpoints; admin endpoints
|
||||
additionally require the verified DID to be in the server-side
|
||||
`ADMIN_DIDS` allowlist.
|
||||
|
||||
servers:
|
||||
- url: https://api.remanso.space
|
||||
description: Production
|
||||
- url: http://localhost:8080
|
||||
description: Local development
|
||||
|
||||
tags:
|
||||
- name: notes
|
||||
description: Read access to stored notes
|
||||
- name: search
|
||||
description: Full-text fuzzy search via OpenSearch
|
||||
- name: webhooks
|
||||
description: Per-user delivery hooks for note events
|
||||
- name: admin
|
||||
description: Endpoints restricted to ADMIN_DIDS
|
||||
- name: auth
|
||||
description: OAuth helpers
|
||||
- name: meta
|
||||
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
tags: [meta]
|
||||
summary: Hello world
|
||||
responses:
|
||||
"200":
|
||||
description: Plain text greeting
|
||||
content:
|
||||
text/plain:
|
||||
schema: { type: string, example: "Hello world" }
|
||||
|
||||
/health:
|
||||
get:
|
||||
tags: [meta]
|
||||
summary: Health probe
|
||||
responses:
|
||||
"200":
|
||||
description: Service is up
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status: { type: string, example: ok }
|
||||
|
||||
/auth/github:
|
||||
get:
|
||||
tags: [auth]
|
||||
summary: GitHub OAuth proxy
|
||||
description: |
|
||||
Exchanges a GitHub OAuth `code` for a token, or refreshes an existing
|
||||
one. The server adds its own client_id/client_secret and forwards to
|
||||
`https://github.com/login/oauth/access_token`. Response body and
|
||||
status are passed through from GitHub.
|
||||
parameters:
|
||||
- in: query
|
||||
name: code
|
||||
required: true
|
||||
schema: { type: string }
|
||||
description: GitHub OAuth code, or the refresh token when `type=refresh`.
|
||||
- in: query
|
||||
name: type
|
||||
required: false
|
||||
schema: { type: string, enum: [refresh] }
|
||||
description: When `refresh`, performs a refresh-token grant instead.
|
||||
responses:
|
||||
"200":
|
||||
description: GitHub access-token response (shape determined by GitHub).
|
||||
content:
|
||||
application/json:
|
||||
schema: { type: object, additionalProperties: true }
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
|
||||
/notes:
|
||||
get:
|
||||
tags: [notes]
|
||||
summary: List discoverable notes across all users
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: A page of notes ordered by rkey descending.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/NotePage" }
|
||||
|
||||
/{did}/notes:
|
||||
get:
|
||||
tags: [notes]
|
||||
summary: List discoverable notes for a single DID
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Did"
|
||||
- $ref: "#/components/parameters/Cursor"
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: A page of notes for the given DID.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/NotePage" }
|
||||
|
||||
/notes/feed:
|
||||
post:
|
||||
tags: [notes]
|
||||
summary: Multi-DID feed
|
||||
description: Paginated discoverable notes across an arbitrary set of DIDs.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [dids]
|
||||
properties:
|
||||
dids:
|
||||
type: array
|
||||
minItems: 1
|
||||
items: { type: string, example: "did:plc:abc123..." }
|
||||
cursor:
|
||||
type: string
|
||||
description: rkey returned as `cursor` from a previous page.
|
||||
limit:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 20
|
||||
responses:
|
||||
"200":
|
||||
description: A page of notes from the requested DIDs.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/NotePage" }
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
|
||||
/search:
|
||||
get:
|
||||
tags: [search]
|
||||
summary: Public full-text fuzzy search
|
||||
description: |
|
||||
Searches every discoverable note's title and content. Uses
|
||||
OpenSearch `multi_match` with `fuzziness: AUTO`, so small typos still
|
||||
match. Each hit includes a sentence-bounded `snippet` from the match
|
||||
with query terms wrapped in `<em>` tags.
|
||||
parameters:
|
||||
- in: query
|
||||
name: q
|
||||
required: true
|
||||
schema: { type: string, minLength: 1 }
|
||||
description: Query string.
|
||||
- in: query
|
||||
name: cursor
|
||||
required: false
|
||||
schema: { type: string }
|
||||
description: Opaque base64 cursor returned from a previous call.
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: A page of search hits.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SearchPage" }
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
|
||||
/{did}/search:
|
||||
get:
|
||||
tags: [search]
|
||||
summary: Owner-scoped full-text fuzzy search
|
||||
description: |
|
||||
Same as `/search` but limited to a single DID and including
|
||||
non-discoverable notes (drafts). Requires a bearer JWT whose `sub`
|
||||
equals the path `:did`.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Did"
|
||||
- in: query
|
||||
name: q
|
||||
required: true
|
||||
schema: { type: string, minLength: 1 }
|
||||
- in: query
|
||||
name: cursor
|
||||
required: false
|
||||
schema: { type: string }
|
||||
- $ref: "#/components/parameters/Limit"
|
||||
responses:
|
||||
"200":
|
||||
description: A page of search hits for the caller's DID.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/SearchPage" }
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"403":
|
||||
$ref: "#/components/responses/Forbidden"
|
||||
|
||||
/{did}/webhooks:
|
||||
get:
|
||||
tags: [webhooks]
|
||||
summary: List a DID's webhook subscriptions
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Did"
|
||||
responses:
|
||||
"200":
|
||||
description: All subscriptions owned by the caller.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/WebhookSubscription" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
post:
|
||||
tags: [webhooks]
|
||||
summary: Add one or more webhook subscriptions
|
||||
description: |
|
||||
When `verb` is omitted, a pair of subscriptions is created — one for
|
||||
`create`, one for `delete`. When `verb` is provided, only that
|
||||
subscription is created. The response array contains every
|
||||
subscription that was inserted.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Did"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [method, url]
|
||||
properties:
|
||||
method:
|
||||
type: string
|
||||
example: POST
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
token:
|
||||
type: string
|
||||
description: Optional bearer token sent to the webhook URL.
|
||||
verb:
|
||||
type: string
|
||||
enum: [create, delete, bulk-create]
|
||||
responses:
|
||||
"201":
|
||||
description: One subscription per requested verb.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/WebhookSubscription" }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
delete:
|
||||
tags: [webhooks]
|
||||
summary: Delete every webhook subscription for the caller
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Did"
|
||||
responses:
|
||||
"204": { description: All subscriptions deleted. }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
/{did}/webhooks/{id}:
|
||||
delete:
|
||||
tags: [webhooks]
|
||||
summary: Delete a single webhook subscription by id
|
||||
security:
|
||||
- bearerAuth: []
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/Did"
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema: { type: integer, minimum: 1 }
|
||||
responses:
|
||||
"204": { description: Subscription deleted. }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
"404":
|
||||
description: No subscription with that id owned by this DID.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
|
||||
/admin/webhooks:
|
||||
get:
|
||||
tags: [admin]
|
||||
summary: List every webhook subscription
|
||||
description: Requires the verified DID to be in `ADMIN_DIDS`.
|
||||
security:
|
||||
- bearerAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: All subscriptions across all DIDs.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/WebhookSubscription" }
|
||||
"401": { $ref: "#/components/responses/Unauthorized" }
|
||||
"403": { $ref: "#/components/responses/Forbidden" }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
AT Protocol PDS session JWT. The server resolves the DID from the
|
||||
token's `sub`, looks up the PDS endpoint via plc.directory, and
|
||||
verifies the session by calling `com.atproto.server.getSession`
|
||||
against that PDS.
|
||||
|
||||
parameters:
|
||||
Did:
|
||||
in: path
|
||||
name: did
|
||||
required: true
|
||||
schema: { type: string, example: "did:plc:abc123..." }
|
||||
description: AT Protocol DID.
|
||||
Cursor:
|
||||
in: query
|
||||
name: cursor
|
||||
required: false
|
||||
schema: { type: string }
|
||||
description: rkey returned as `cursor` from the previous page.
|
||||
Limit:
|
||||
in: query
|
||||
name: limit
|
||||
required: false
|
||||
schema: { type: integer, minimum: 1, default: 20 }
|
||||
description: Maximum results per page. Search endpoints cap at 100.
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Malformed input.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
Unauthorized:
|
||||
description: Missing or invalid bearer token.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
Forbidden:
|
||||
description: Token is valid but the caller is not authorized for this resource.
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: "#/components/schemas/Error" }
|
||||
|
||||
schemas:
|
||||
Error:
|
||||
type: object
|
||||
required: [error]
|
||||
properties:
|
||||
error: { type: string }
|
||||
|
||||
Note:
|
||||
type: object
|
||||
required: [did, rkey, title, publishedAt, createdAt]
|
||||
properties:
|
||||
did: { type: string }
|
||||
rkey: { type: string }
|
||||
title: { type: string }
|
||||
publishedAt: { type: string, format: date-time }
|
||||
createdAt: { type: string, format: date-time }
|
||||
language:
|
||||
type: string
|
||||
description: ISO 639-3 code if known.
|
||||
|
||||
NotePage:
|
||||
type: object
|
||||
required: [notes]
|
||||
properties:
|
||||
notes:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/Note" }
|
||||
cursor:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Present only when more pages exist; pass back as `cursor`.
|
||||
|
||||
SearchHit:
|
||||
type: object
|
||||
required: [did, rkey, title, snippet, score]
|
||||
properties:
|
||||
did: { type: string }
|
||||
rkey: { type: string }
|
||||
title: { type: string }
|
||||
snippet:
|
||||
type: string
|
||||
description: Sentence-bounded fragment with `<em>` around matches.
|
||||
score: { type: number }
|
||||
publishedAt: { type: string, format: date-time }
|
||||
|
||||
SearchPage:
|
||||
type: object
|
||||
required: [results]
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items: { $ref: "#/components/schemas/SearchHit" }
|
||||
cursor:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Opaque base64 cursor; pass back as `cursor`.
|
||||
|
||||
WebhookSubscription:
|
||||
type: object
|
||||
required: [id, did, method, url, verb]
|
||||
description: |
|
||||
`token` is write-only — it is accepted by `POST /:did/webhooks` but
|
||||
never returned in responses.
|
||||
properties:
|
||||
id: { type: integer, minimum: 1 }
|
||||
did: { type: string }
|
||||
method: { type: string, example: POST }
|
||||
url: { type: string, format: uri }
|
||||
verb: { type: string, enum: [create, delete, bulk-create] }
|
||||
Reference in New Issue
Block a user