450 lines
14 KiB
YAML
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] }
|