forked from mirror/crumbs-original
- FEATURES.md: add public link sharing feature details - ARCHITECTURE.md: add shared_notes table to schema section - openapi.yaml: add share create/revoke, public note, and public attachment endpoints with SharedNoteData schema - Regenerate API.md Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
795 lines
20 KiB
YAML
795 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:
|
|
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/Attachment'
|
|
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
|