Files
crumbs/docs/openapi.yaml
T
Frederic CabassutandClaude Opus 4.6 92d0581186 fix: address PR review — sanitize attachments, SSR safety, a11y
- Strip server paths from public attachment responses (new PublicAttachment type)
- Use $page.url.origin instead of window.location.origin (SSR-safe)
- Fix toggle switch left-4.5 → left-[1.125rem] (valid Tailwind)
- Add aria-label to SharingIndicator button for screen readers
- Add opportunistic pruning to IP rate limiter to prevent unbounded growth
- Reduce shared attachment cache to 1 hour (respect share revocation)
- Update OpenAPI schema with PublicAttachment type
- Add unit test asserting server paths are not leaked

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 22:06:41 +01:00

813 lines
20 KiB
YAML

openapi: 3.1.0
info:
title: Crumbs API
description: REST API for Crumbs by Bretzel — a self-hostable, offline-first notes app.
version: 0.1.0
contact:
name: Bretzel
url: https://bretzel.app
servers:
- url: http://localhost:3000
description: Local development
security:
- sessionCookie: []
paths:
/api/auth/setup:
post:
summary: First-time password setup
description: Creates the single user account. Only works once — returns 400 if already set up.
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [password]
properties:
password:
type: string
minLength: 8
responses:
'201':
description: Account created, session cookie set
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'400':
description: Setup already completed or invalid password
/api/auth/login:
post:
summary: Log in
tags: [Auth]
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [password]
properties:
password:
type: string
responses:
'200':
description: Logged in, session cookie set
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'401':
description: Invalid password
/api/auth/logout:
post:
summary: Log out
tags: [Auth]
responses:
'200':
description: Session cleared
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
/api/notes:
get:
summary: List notes
tags: [Notes]
parameters:
- name: filter
in: query
schema:
type: string
enum: [all, archived, trashed]
default: all
responses:
'200':
description: Array of notes
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
post:
summary: Create a note
tags: [Notes]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NoteInput'
responses:
'201':
description: Created note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
/api/notes/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
summary: Get a note
tags: [Notes]
responses:
'200':
description: Note with tags
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
'404':
description: Note not found
patch:
summary: Update a note
description: Partial update — only include changed fields.
tags: [Notes]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NoteInput'
responses:
'200':
description: Updated note
content:
application/json:
schema:
$ref: '#/components/schemas/Note'
'404':
description: Note not found
delete:
summary: Permanently delete a note
tags: [Notes]
responses:
'200':
description: Deleted
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'404':
description: Note not found
/api/notes/{id}/collaborators:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
summary: List collaborators
description: Returns all collaborators for a note. Requires access (owner or collaborator).
tags: [Collaboration]
responses:
'200':
description: Collaborator list
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Collaborator'
post:
summary: Add a collaborator
description: Owner-only. Adds a user as collaborator on the note.
tags: [Collaboration]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [userId]
properties:
userId:
type: integer
responses:
'201':
description: Collaborator added, returns updated list
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Collaborator'
'400':
description: Invalid request (missing userId, owner as collaborator)
'409':
description: User is already a collaborator
delete:
summary: Remove a collaborator
description: Owner can remove anyone. Collaborator can only remove self (leave). Use `userId=self` to leave.
tags: [Collaboration]
parameters:
- name: userId
in: query
required: true
schema:
oneOf:
- type: integer
- type: string
enum: [self]
responses:
'200':
description: Collaborator removed
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
/api/notes/{id}/share:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
post:
summary: Create a public share link
description: Owner-only. Creates a public share URL for the note. Idempotent — returns existing token if already shared.
tags: [Sharing]
responses:
'201':
description: Share link created
content:
application/json:
schema:
type: object
properties:
token:
type: string
url:
type: string
format: uri
'404':
description: Note not found or not owner
delete:
summary: Revoke a public share link
description: Owner-only. Removes the public share link for the note.
tags: [Sharing]
responses:
'204':
description: Share link revoked
'404':
description: Note not found or not owner
/api/shared/{token}:
parameters:
- name: token
in: path
required: true
schema:
type: string
get:
summary: Get a shared note (public)
description: No authentication required. Returns the note content for a valid share token. Rate limited to 60 requests per minute per IP.
tags: [Sharing]
security: []
responses:
'200':
description: Shared note data
content:
application/json:
schema:
$ref: '#/components/schemas/SharedNoteData'
'404':
description: Invalid token or note is trashed
'429':
description: Rate limit exceeded
/api/shared/{token}/attachment/{attachmentId}:
parameters:
- name: token
in: path
required: true
schema:
type: string
- name: attachmentId
in: path
required: true
schema:
type: string
get:
summary: Get an attachment from a shared note (public)
description: No authentication required. Serves image binary for an attachment belonging to the shared note. Rate limited.
tags: [Sharing]
security: []
parameters:
- name: thumb
in: query
schema:
type: string
enum: ['1']
description: Set to '1' to get the thumbnail version
responses:
'200':
description: Image binary
content:
image/*:
schema:
type: string
format: binary
'404':
description: Invalid token, note trashed, or attachment not found
'429':
description: Rate limit exceeded
/api/users/search:
get:
summary: Search users
description: Search registered users by email or display name. Excludes the current user. Any authenticated user can access.
tags: [Users]
parameters:
- name: q
in: query
required: true
schema:
type: string
description: Search query (matches email or displayName)
responses:
'200':
description: Matching users (max 10)
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
displayName:
type: string
email:
type: string
/api/notes/{id}/attachments:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
summary: List or download attachments
description: Without `attachmentId`, lists all attachments. With `attachmentId`, returns the file.
tags: [Attachments]
parameters:
- name: attachmentId
in: query
schema:
type: string
- name: thumb
in: query
description: Set to "1" to return the thumbnail instead of the full image (requires attachmentId)
schema:
type: string
enum: ['1']
responses:
'200':
description: Attachment list (without attachmentId) or file content (with attachmentId)
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Attachment'
post:
summary: Upload an image attachment
tags: [Attachments]
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file]
properties:
file:
type: string
format: binary
description: Image file (max 10MB)
thumbnail:
type: string
format: binary
description: Pre-generated thumbnail (WebP, ~200px)
responses:
'201':
description: Attachment metadata
content:
application/json:
schema:
$ref: '#/components/schemas/Attachment'
'400':
description: Invalid file
delete:
summary: Delete an attachment
tags: [Attachments]
parameters:
- name: attachmentId
in: query
required: true
schema:
type: string
responses:
'200':
description: Deleted
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
/api/search:
get:
summary: Full-text search
description: Searches across note titles, content, and tags. Excludes trashed notes.
tags: [Search]
parameters:
- name: q
in: query
required: true
schema:
type: string
responses:
'200':
description: Matching notes
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
/api/tags:
get:
summary: List all tags
tags: [Tags]
responses:
'200':
description: Array of tags
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Tag'
/api/sync:
get:
summary: Pull changes since timestamp
tags: [Sync]
parameters:
- name: since
in: query
required: true
schema:
type: integer
description: Unix timestamp in milliseconds
responses:
'200':
description: Notes updated since the given timestamp
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Note'
post:
summary: Push local changes
tags: [Sync]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [changes]
properties:
changes:
type: array
items:
$ref: '#/components/schemas/SyncChange'
responses:
'200':
description: Changes applied
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'400':
description: Invalid changes payload
/api/settings/api-keys:
get:
summary: List API keys
tags: [Settings]
responses:
'200':
description: List of API keys (without hashes)
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ApiKey'
post:
summary: Create API key
tags: [Settings]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [name]
properties:
name:
type: string
responses:
'201':
description: API key created (key shown only once)
content:
application/json:
schema:
type: object
properties:
id:
type: string
key:
type: string
name:
type: string
keyPrefix:
type: string
/api/settings/api-keys/{id}:
delete:
summary: Revoke API key
tags: [Settings]
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: Key revoked
content:
application/json:
schema:
$ref: '#/components/schemas/Success'
'404':
description: Key not found
/api/mcp:
post:
summary: MCP Streamable HTTP endpoint
description: Model Context Protocol endpoint. Accepts JSON-RPC 2.0 messages. Authenticate via Bearer token (API key).
tags: [MCP]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
description: JSON-RPC 2.0 message
responses:
'200':
description: JSON-RPC response (may be SSE stream)
'400':
description: Invalid session
'401':
description: Invalid or missing API key
components:
securitySchemes:
sessionCookie:
type: apiKey
in: cookie
name: session
bearerAuth:
type: http
scheme: bearer
description: API key (crumbs_...)
schemas:
PublicAttachment:
type: object
description: Sanitized attachment metadata for public sharing (no server paths)
properties:
id:
type: string
filename:
type: string
mimeType:
type: string
size:
type: integer
featured:
type: boolean
createdAt:
type: string
format: date-time
SharedNoteData:
type: object
properties:
title:
type: string
content:
type: string
checklistMode:
type: boolean
color:
type: string
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
attachments:
type: array
items:
$ref: '#/components/schemas/PublicAttachment'
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
Success:
type: object
properties:
success:
type: boolean
Note:
type: object
properties:
id:
type: string
format: uuid
title:
type: string
content:
type: string
color:
type: string
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
pinned:
type: boolean
archived:
type: boolean
trashed:
type: boolean
trashedAt:
type: string
format: date-time
nullable: true
checklistMode:
type: boolean
sortOrder:
type: integer
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
version:
type: integer
tags:
type: array
items:
type: string
attachments:
type: array
items:
$ref: '#/components/schemas/Attachment'
NoteInput:
type: object
properties:
id:
type: string
format: uuid
description: Optional — generated if omitted
title:
type: string
content:
type: string
color:
type: string
enum: [default, coral, peach, sand, mint, sage, fog, storm, dusk, blossom, clay, chalk]
pinned:
type: boolean
archived:
type: boolean
trashed:
type: boolean
checklistMode:
type: boolean
sortOrder:
type: integer
Tag:
type: object
properties:
id:
type: integer
name:
type: string
Attachment:
type: object
properties:
id:
type: string
noteId:
type: string
filename:
type: string
mimeType:
type: string
size:
type: integer
thumbnailPath:
type: string
nullable: true
description: Server path to thumbnail file (null if no thumbnail)
createdAt:
type: string
format: date-time
SyncChange:
type: object
required: [noteId, operation, timestamp]
properties:
noteId:
type: string
format: uuid
operation:
type: string
enum: [create, update, delete]
timestamp:
type: integer
description: Unix timestamp in milliseconds
data:
$ref: '#/components/schemas/NoteInput'
Collaborator:
type: object
properties:
userId:
type: integer
displayName:
type: string
email:
type: string
addedAt:
type: string
format: date-time
ApiKey:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
keyPrefix:
type: string
description: First 12 characters of the key for display
createdAt:
type: string
format: date-time
lastUsedAt:
type: string
format: date-time
nullable: true