Files
llink/go/docs/api.md
T
2026-02-21 08:48:34 -08:00

618 lines
16 KiB
Markdown

# API Documentation
All protected endpoints require a `Bearer` token in the `Authorization` header.
## Authentication
### Request Sign-In Code
`POST /auth/request-code`
Sends a 4-digit sign-in code to the provided email. Creates user if not exists.
**Request Body:**
```json
{
"email": "user@example.com"
}
```
**Response:** `204 No Content`
### Sign In
`POST /auth/sign-in`
Verifies the code and returns a session token.
**Request Body:**
```json
{
"email": "user@example.com",
"code": "1234"
}
```
**Response:**
```json
{
"human": { "id": "...", "email": "...", "email_prefix": "...", "created_at": "..." },
"token": "session_token"
}
```
### Sign Out
`POST /auth/sign-out` (Protected)
Invalidates the current session. Returns `204 No Content`.
### Get Current User
`GET /auth/me` (Protected)
Returns the authenticated user.
---
## Startup
### Get Startup Data
`GET /startup` (Protected)
Bootstrap endpoint for initial app load. Returns all networks the user belongs to, with all streams and their particles fully enriched.
**Response:**
```json
{
"networks": [
{
"id": "net-456",
"name": "My Team",
"admin_human": { "id": "...", "email": "...", "email_prefix": "...", "created_at": "..." },
"humans": [{ "id": "...", "email": "...", "email_prefix": "...", "created_at": "..." }],
"open_stream_count": 2,
"open_stream_capacity": 5,
"created_at": "2025-01-15T10:30:00Z",
"streams": [
{
"id": "p-001",
"name": "Sprint Planning",
"description": "Weekly sync",
"status": "open",
"members": ["alice@example.com"],
"particles": [
{
"id": "p-002",
"type": "text",
"data": { "content": "Hello" },
"created_by_email": "alice@example.com",
"seen": true,
"acks": [],
"updated_at": "...",
"created_at": "..."
}
],
"unseen_count": 0
}
]
}
]
}
```
---
## Networks
### Create Network
`POST /networks` (Protected)
**Request Body:**
```json
{
"name": "My Network"
}
```
### List Networks
`GET /networks` (Protected)
Returns all networks the user is a member of.
### Get Network
`GET /networks/{id}` (Protected)
Returns a specific network by ID.
### Add Members to Network
`POST /networks/{id}/members` (Protected)
**Request Body:**
```json
{
"email_addresses": ["user1@example.com", "user2@example.com"]
}
```
### Remove Member from Network
`DELETE /networks/{id}/members/{email}` (Protected)
Removes a member by email from the network.
### Set Open Stream Capacity
`PUT /networks/{id}/capacity` (Protected, Admin only)
**Request Body:**
```json
{
"capacity": 10
}
```
---
## Streams
Streams are top-level particles of type `stream`. They have dedicated endpoints for creation and management, and contain child particles.
### Create Stream
`POST /networks/{network_id}/streams` (Protected)
**Request Body:**
```json
{
"name": "Sprint Planning",
"description": "Weekly sync",
"visibility": "custom",
"members": ["user@example.com"]
}
```
- `visibility`: `network_all` (default) or `custom`
- `members` is required when visibility is `custom`
**Response:** `201 Created` — returns a [Stream](#stream-1) object.
### Get Stream
`GET /streams/{id}` (Protected)
Returns a stream with all its child particles, enriched with seen/ack state.
**Response:** returns a [Stream](#stream-1) object.
### Update Stream
`PATCH /streams/{id}` (Protected)
Updates a stream's name and/or description. Status is not affected (use the open/close endpoints instead). Only provided fields are updated.
**Request Body:**
```json
{
"name": "New Name",
"description": "New description"
}
```
- Both fields are optional — omit a field to leave it unchanged
- `name` cannot be empty if provided
**Response:** returns the updated [Stream](#stream-1) object.
### Create Stream Particle
`POST /streams/{id}/particles` (Protected)
Creates a child particle inside a stream. Child particles inherit visibility from the stream.
**Request Body:**
```json
{
"type": "text|media|file|quest|paper",
"data": {}
}
```
- Cannot create `stream` or `folder` types as children
- For `media` and `file` types, `data` must include a valid `object_id` from depot
**Response:** `201 Created` — returns a [StreamParticle](#streamparticle) object.
### Open Stream
`POST /streams/{id}/open` (Protected)
Opens a closed stream. Fails with `409` if capacity would be exceeded.
### Close Stream
`POST /streams/{id}/close` (Protected)
Closes an open stream.
### Add Members to Stream
`POST /streams/{id}/members` (Protected)
**Request Body:**
```json
{
"emails": ["user@example.com"]
}
```
### Remove Members from Stream
`DELETE /streams/{id}/members` (Protected)
**Request Body:**
```json
{
"emails": ["user@example.com"]
}
```
---
## Particles
### List Particles
`GET /networks/{network_id}/particles` (Protected)
**Query Parameters:**
- `parent_id` (optional): Filter by parent particle
- `cursor` (optional): Pagination cursor
- `direction` (optional): `after` or `before` (default: `after`)
- `type` (optional, repeatable): Filter by particle type
**Response enrichment:**
Each particle in the response includes:
- `seen` (boolean): Whether the requester has marked this particle as seen
- `acks` (array): List of acknowledgments `[{email, acked_at}]`
- `unseen_count` (integer, streams only): Count of unseen child particles
### Get Particle
`GET /particles/{id}` (Protected)
### Update Particle
`PATCH /particles/{id}` (Protected)
**Request Body:**
```json
{
"data": {}
}
```
### Delete Particle
`DELETE /particles/{id}` (Protected)
Deletes the particle and all children. If it references a depot object, that is also deleted.
### Download Particle
`GET /particles/{id}/download` (Protected)
Returns a `302` redirect to a signed download URL. Only works for `media` and `file` particles.
### Mark Seen
`POST /particles/{id}/seen` (Protected)
Marks a particle as seen by the requester. This is private state, only visible to the requester.
Returns `204 No Content` on success.
### Mark Seen (Batch)
`POST /particles/seen` (Protected)
Marks multiple particles as seen by the requester.
**Request Body:**
```json
{
"particle_ids": ["particle_uuid1", "particle_uuid2"]
}
```
Returns `204 No Content` on success.
### Acknowledge Particle
`POST /particles/{id}/ack` (Protected)
Acknowledges a particle. Acknowledgments are public and permanent, visible to all users with access. Also marks the particle as seen.
Returns `204 No Content` on success.
---
## Depot (File Storage)
### Prepare Upload
`POST /depot/upload` (Protected)
Prepares a signed URL for direct upload to GCS.
**Request Body:**
```json
{
"network_id": "network_uuid",
"name": "filename.png",
"content_type": "image/png",
"content_length": 12345
}
```
**Response:**
```json
{
"object_id": "uuid",
"upload_url": "https://storage.googleapis.com/...",
"upload_headers": { "Content-Type": "image/png" }
}
```
### Confirm Upload
`POST /depot/objects/{id}/confirm` (Protected)
Confirms that an upload has been completed.
**Response:**
```json
{
"id": "uuid",
"name": "filename.png",
"content_type": "image/png",
"content_length": 12345,
"contains_content": true,
"created_at": "..."
}
```
### Upload Flow
1. `POST /depot/upload` — get signed URL and `object_id`
2. Upload file directly to GCS using the signed URL
3. `POST /depot/objects/{id}/confirm` — mark upload complete
4. Create a `media` or `file` particle with the `object_id` in its data
---
## Object Reference
### Human
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string \| null` | Unique identifier. Null if not yet registered. |
| `email` | `string` | Email address (stable identifier). |
| `email_prefix` | `string` | The local part of the email (before `@`). |
| `created_at` | `string \| null` | ISO 8601 timestamp. Null if not registered. |
```json
{
"id": "abc-123",
"email": "alice@example.com",
"email_prefix": "alice",
"created_at": "2025-01-15T10:30:00Z"
}
```
### Network
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. |
| `name` | `string` | Display name of the network. |
| `admin_human` | `Human` | The network administrator. |
| `humans` | `Human[]` | All members of the network (including admin). |
| `open_stream_count` | `integer` | Number of currently open streams. |
| `open_stream_capacity` | `integer` | Maximum number of concurrent open streams (default: 5). |
| `created_at` | `string` | ISO 8601 timestamp. |
```json
{
"id": "net-456",
"name": "My Team",
"admin_human": { "id": "abc-123", "email": "alice@example.com", "email_prefix": "alice", "created_at": "..." },
"humans": [
{ "id": "abc-123", "email": "alice@example.com", "email_prefix": "alice", "created_at": "..." }
],
"open_stream_count": 2,
"open_stream_capacity": 5,
"created_at": "2025-01-15T10:30:00Z"
}
```
### Stream
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier (this is a particle ID). |
| `name` | `string` | Stream name. |
| `description` | `string` | Stream description. |
| `status` | `string` | `"open"`, `"closed"`, or `"unspecified"`. |
| `members` | `string[]` | Emails of stream members. Omitted for `network_all` visibility. |
| `particles` | `StreamParticle[]` | Child particles in the stream. |
| `unseen_count` | `integer` | Number of unseen child particles for the requester. |
```json
{
"id": "p-001",
"name": "Sprint Planning",
"description": "Weekly sync",
"status": "open",
"members": ["alice@example.com"],
"particles": [],
"unseen_count": 0
}
```
### StreamParticle
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. |
| `type` | `string` | One of: `media`, `file`, `text`, `quest`, `paper`. |
| `data` | `object` | Type-specific payload (see [Particle Data by Type](#particle-data-by-type)). |
| `created_by_email` | `string` | Email of the creator. |
| `seen` | `boolean` | Whether the requester has seen this particle. |
| `acks` | `AckInfo[]` | Acknowledgments from users. |
| `updated_at` | `string` | ISO 8601 timestamp. |
| `created_at` | `string` | ISO 8601 timestamp. |
### Particle
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique identifier. |
| `type` | `string` | One of: `stream`, `folder`, `media`, `file`, `text`, `quest`, `paper`. |
| `network_id` | `string` | The network this particle belongs to. |
| `parent_id` | `string \| null` | Parent particle ID, if nested. |
| `created_by_email` | `string` | Email of the creator. |
| `visibility` | `string` | `"network_all"`, `"custom"`, or `"inherited"`. |
| `stream_status` | `string \| null` | Only on `stream` type: `"open"` or `"closed"`. |
| `data` | `object` | Type-specific payload (see [Particle Data by Type](#particle-data-by-type)). |
| `download_url` | `string \| null` | Signed download URL. Only on `media`/`file` particles. |
| `seen` | `boolean \| null` | Whether the requester has seen this particle. Only in list responses. |
| `acks` | `AckInfo[]` | Acknowledgments. Only in list responses. |
| `unseen_count` | `integer \| null` | Unseen child count. Only on `stream` particles in list responses. |
| `updated_at` | `string` | ISO 8601 timestamp. |
| `created_at` | `string` | ISO 8601 timestamp. |
### AckInfo
| Field | Type | Description |
|-------|------|-------------|
| `email` | `string` | Email of the user who acknowledged. |
| `acked_at` | `string` | ISO 8601 timestamp of the acknowledgment. |
### ParticleList
Returned by `GET /networks/{network_id}/particles`.
| Field | Type | Description |
|-------|------|-------------|
| `particles` | `Particle[]` | Array of enriched particle objects. |
| `has_more` | `boolean` | Whether more results exist beyond this page. |
| `next_cursor` | `string \| null` | Cursor to fetch the next page. |
| `prev_cursor` | `string \| null` | Cursor to fetch the previous page. |
### DepotObject
Returned by `POST /depot/objects/{id}/confirm`.
| Field | Type | Description |
|-------|------|-------------|
| `id` | `string` | Unique object identifier. |
| `name` | `string` | Original filename. |
| `content_type` | `string` | MIME type (e.g. `image/png`). |
| `content_length` | `integer` | Size in bytes. |
| `contains_content` | `boolean` | Whether the object has been uploaded successfully. |
| `created_at` | `string` | ISO 8601 timestamp. |
---
## Particle Data by Type
The `data` field on a Particle is a JSON object whose schema depends on the particle's `type`.
### `stream`
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Stream name (required). |
| `status` | `string` | `"open"` or `"closed"` (required). |
| `description` | `string \| null` | Stream description (optional). |
```json
{ "name": "Sprint Planning", "status": "open", "description": "Weekly sync" }
```
### `folder`
| Field | Type | Description |
|-------|------|-------------|
| `name` | `string` | Folder name (required). |
| `color` | `string \| null` | Display color (optional). |
```json
{ "name": "Design Assets", "color": "#FF5733" }
```
### `media`
| Field | Type | Description |
|-------|------|-------------|
| `object_id` | `string` | Reference to depot storage object (required). |
| `mime_type` | `string` | MIME type of the media (required). |
| `duration_ms` | `integer` | Duration in milliseconds, must be > 0 (required). |
```json
{ "object_id": "obj-123", "mime_type": "image/jpeg", "duration_ms": 5000 }
```
### `file`
| Field | Type | Description |
|-------|------|-------------|
| `object_id` | `string` | Reference to depot storage object (required). |
| `filename` | `string` | Original filename (required). |
| `mime_type` | `string` | MIME type (required). |
| `size` | `integer` | File size in bytes, must be > 0 (required). |
```json
{ "object_id": "obj-456", "filename": "report.pdf", "mime_type": "application/pdf", "size": 204800 }
```
### `text`
| Field | Type | Description |
|-------|------|-------------|
| `content` | `string` | Text content (required). |
```json
{ "content": "Hello world" }
```
### `quest`
| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Quest/task title (required). |
| `description` | `string` | Details about the quest (required). |
| `status` | `string \| null` | Current status (optional, e.g. `"todo"`, `"in_progress"`, `"done"`). |
| `assigned_to` | `string \| null` | Email of the assigned user (optional). |
| `due_date` | `string \| null` | ISO date string (optional, e.g. `"2025-03-01"`). |
```json
{ "title": "Fix login bug", "description": "Login fails on Safari", "status": "todo", "assigned_to": "bob@example.com" }
```
### `paper`
| Field | Type | Description |
|-------|------|-------------|
| `title` | `string` | Document title (required). |
| `content` | `string` | Document body in markdown (required). |
```json
{ "title": "Architecture RFC", "content": "## Overview\n..." }
```
---
## Visibility
Particles support three visibility modes:
| Mode | Description |
|------|-------------|
| `network_all` | Visible to all network members. |
| `custom` | Visible only to specified members (requires `members` list). |
| `inherited` | Inherits visibility from parent particle. Used for child particles in streams. |
Root-level particles (streams, folders) use `network_all` or `custom`. Child particles created via `POST /streams/{id}/particles` automatically use `inherited`.
---
## Error Responses
All endpoints return standard HTTP status codes with plain text error bodies:
- **400** Bad Request — invalid input or missing required fields
- **401** Unauthorized — missing or invalid auth token
- **403** Forbidden — user lacks permission (not a member, not creator, not admin)
- **404** Not Found — resource doesn't exist
- **409** Conflict — state conflict (stream already open/closed, capacity exceeded)
- **500** Internal Server Error