Files
remanso-jetstream/openapi.yaml
2026-06-07 11:29:02 +02:00

450 lines
14 KiB
YAML

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] }