{
  "openapi": "3.1.0",
  "info": {
    "title": "Distributed Creatives API",
    "version": "1.0.0",
    "description": "REST API for Distributed Creatives, a 501(c)(3) nonprofit infrastructure organization building creator-centered systems for rights protection, fair value exchange, and long-term cultural preservation. Public endpoints (Content, Donation, Health) require no authentication and have open CORS. Auth and Membership endpoints use cookie-based session authentication.",
    "contact": {
      "name": "Distributed Creatives",
      "url": "https://distributedcreatives.org",
      "email": "hello@distributedcreatives.org"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/licenses/MIT"
    }
  },
  "servers": [
    {
      "url": "https://distributedcreatives.org",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "API health and status"
    },
    {
      "name": "Content",
      "description": "Public content endpoints — organization info, projects, and team data. No authentication required."
    },
    {
      "name": "Donation",
      "description": "Donation URL generation via Every.org. No authentication required. Open CORS."
    },
    {
      "name": "Auth",
      "description": "Authentication endpoints — login, logout, session check, and password recovery. Protected endpoints require a session cookie."
    },
    {
      "name": "Membership",
      "description": "Membership management — signup, profile, directory, and member count."
    },
    {
      "name": "Updates",
      "description": "Project updates — public read access, admin-only write access."
    },
    {
      "name": "Telemetry",
      "description": "Event tracking for analytics. Rate-limited."
    },
    {
      "name": "Admin",
      "description": "Admin-only endpoints. Require session cookie with admin privileges."
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "tags": ["Health"],
        "summary": "API health check",
        "description": "Returns the current health status of the API. No authentication required.",
        "responses": {
          "200": {
            "description": "API is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "ok": true,
                  "timestamp": "2026-03-16T12:00:00.000Z",
                  "runtime": "cloudflare-pages-functions"
                }
              }
            }
          }
        }
      }
    },
    "/v1/content/org": {
      "get": {
        "operationId": "getOrganization",
        "tags": ["Content"],
        "summary": "Get organization metadata",
        "description": "Returns structured information about Distributed Creatives including legal name, EIN, tax status, address, and contact details. No authentication required. Open CORS.",
        "responses": {
          "200": {
            "description": "Organization metadata",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string" },
                "description": "Caching directive",
                "example": "public, max-age=300, s-maxage=600"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Organization"
                }
              }
            }
          }
        }
      }
    },
    "/v1/content/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": ["Content"],
        "summary": "List all projects",
        "description": "Returns the full list of Distributed Creatives projects. No authentication required. Open CORS.",
        "responses": {
          "200": {
            "description": "Project list",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string" },
                "example": "public, max-age=300, s-maxage=600"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    }
                  },
                  "required": ["projects"]
                }
              }
            }
          }
        }
      }
    },
    "/v1/content/projects/{projectId}": {
      "get": {
        "operationId": "getProject",
        "tags": ["Content"],
        "summary": "Get a single project by ID",
        "description": "Returns detailed information for one project. No authentication required. Open CORS.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Project identifier",
            "schema": {
              "type": "string",
              "enum": ["stc", "lan", "peers", "everarchive"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "404": {
            "description": "Project not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "message": "Project not found",
                  "code": "NOT_FOUND"
                }
              }
            }
          }
        }
      }
    },
    "/v1/content/team": {
      "get": {
        "operationId": "listTeam",
        "tags": ["Content"],
        "summary": "List team members",
        "description": "Returns the public team directory. No authentication required. Open CORS.",
        "responses": {
          "200": {
            "description": "Team member list",
            "headers": {
              "Cache-Control": {
                "schema": { "type": "string" },
                "example": "public, max-age=300, s-maxage=600"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "team": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TeamMember"
                      }
                    }
                  },
                  "required": ["team"]
                }
              }
            }
          }
        }
      }
    },
    "/v1/content/team/{memberId}": {
      "get": {
        "operationId": "getTeamMember",
        "tags": ["Content"],
        "summary": "Get a single team member by ID",
        "description": "Returns details for one team member. No authentication required. Open CORS.",
        "parameters": [
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "description": "Team member identifier (slug)",
            "schema": {
              "type": "string"
            },
            "example": "grig-bilham"
          }
        ],
        "responses": {
          "200": {
            "description": "Team member details",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TeamMember"
                }
              }
            }
          },
          "404": {
            "description": "Team member not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/donate": {
      "get": {
        "operationId": "getDonateInfo",
        "tags": ["Donation"],
        "summary": "Get donation information and instructions",
        "description": "Returns organization info, the donation URL template, and instructions for generating a donation URL via POST. No authentication required. Open CORS.",
        "responses": {
          "200": {
            "description": "Donation info and instructions",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DonateInfoResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createDonateUrl",
        "tags": ["Donation"],
        "summary": "Generate a donation checkout URL",
        "description": "Accepts an optional amount and frequency, returns a constructed Every.org checkout URL. No authentication required. Open CORS. Body may be empty for a donor-chosen amount.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DonationRequest"
              },
              "examples": {
                "one-time-50": {
                  "summary": "One-time $50 donation",
                  "value": { "amount": 50, "frequency": "one-time" }
                },
                "monthly-25": {
                  "summary": "Monthly $25 donation",
                  "value": { "amount": 25, "frequency": "monthly" }
                },
                "open-amount": {
                  "summary": "Donor-chosen amount",
                  "value": {}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Donation URL generated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DonationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "examples": {
                  "invalid-amount": {
                    "value": { "message": "amount must be a positive number", "code": "INVALID_AMOUNT" }
                  },
                  "amount-too-large": {
                    "value": { "message": "amount exceeds maximum of $1,000,000", "code": "AMOUNT_TOO_LARGE" }
                  },
                  "invalid-frequency": {
                    "value": { "message": "frequency must be \"one-time\" or \"monthly\"", "code": "INVALID_FREQUENCY" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/login": {
      "post": {
        "operationId": "login",
        "tags": ["Auth"],
        "summary": "Log in with email and password",
        "description": "Authenticates a user and returns a session cookie. Rate-limited by IP and email.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Login successful. Session cookie set via Set-Cookie header.",
            "headers": {
              "Set-Cookie": {
                "schema": { "type": "string" },
                "description": "Session cookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing email or password",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Invalid credentials",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": { "message": "Invalid email or password", "code": "INVALID_CREDENTIALS" }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "503": {
            "description": "Server configuration error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/auth/logout": {
      "post": {
        "operationId": "logout",
        "tags": ["Auth"],
        "summary": "Log out and invalidate session",
        "description": "Clears the session cookie and bumps the user's session version to invalidate all active sessions. Works even without a valid session (cookie is still cleared).",
        "security": [{ "sessionCookie": [] }],
        "responses": {
          "200": {
            "description": "Logout successful. Session cookie cleared.",
            "headers": {
              "Set-Cookie": {
                "schema": { "type": "string" },
                "description": "Cleared session cookie"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "message": { "type": "string" }
                  },
                  "required": ["ok", "message"]
                },
                "example": { "ok": true, "message": "Logged out" }
              }
            }
          }
        }
      }
    },
    "/v1/auth/session": {
      "get": {
        "operationId": "getSession",
        "tags": ["Auth"],
        "summary": "Check current session status",
        "description": "Returns whether the caller is authenticated and, if so, the current user profile. Always returns 200 — the `authenticated` field indicates status.",
        "security": [{ "sessionCookie": [] }],
        "responses": {
          "200": {
            "description": "Session status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SessionResponse"
                },
                "examples": {
                  "authenticated": {
                    "summary": "Authenticated user",
                    "value": {
                      "authenticated": true,
                      "user": {
                        "id": "abc-123",
                        "fullName": "Jane Doe",
                        "email": "jane@example.com",
                        "membershipTier": "creator",
                        "status": "active",
                        "bio": "",
                        "createdAt": "2026-01-01T00:00:00.000Z",
                        "updatedAt": "2026-01-01T00:00:00.000Z"
                      }
                    }
                  },
                  "unauthenticated": {
                    "summary": "No active session",
                    "value": { "authenticated": false }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/recovery/request": {
      "post": {
        "operationId": "requestPasswordRecovery",
        "tags": ["Auth"],
        "summary": "Request a password recovery email",
        "description": "Sends a password recovery email if the account exists. Always returns the same generic response regardless of whether the email exists, to prevent enumeration attacks. Rate-limited by IP and email.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "Email address of the account to recover"
                  }
                },
                "required": ["email"]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Recovery request accepted (always returns the same message)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "message": { "type": "string" }
                  },
                  "required": ["ok", "message"]
                },
                "example": {
                  "ok": true,
                  "message": "If the account exists, recovery instructions have been sent."
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/recovery/confirm": {
      "post": {
        "operationId": "confirmPasswordRecovery",
        "tags": ["Auth"],
        "summary": "Confirm password recovery with token",
        "description": "Resets the user's password using a valid recovery token. The token is from the recovery email. All existing sessions are invalidated on success. Rate-limited.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecoveryConfirmRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password reset successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "message": { "type": "string" }
                  },
                  "required": ["ok", "message"]
                },
                "example": { "ok": true, "message": "Password has been reset." }
              }
            }
          },
          "400": {
            "description": "Validation error or invalid/expired token",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/memberships/signup": {
      "post": {
        "operationId": "signup",
        "tags": ["Membership"],
        "summary": "Create a new membership account",
        "description": "Registers a new member. The account starts in 'pending' status. Rate-limited by IP and email.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SignupRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Signup successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "message": { "type": "string" }
                  },
                  "required": ["ok", "message"]
                },
                "example": { "ok": true, "message": "Membership request submitted." }
              }
            }
          },
          "400": {
            "description": "Validation error (missing fields, short password, invalid tier)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "409": {
            "description": "Email already registered",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": { "message": "An account with this email already exists", "code": "EMAIL_EXISTS" }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/memberships/me": {
      "get": {
        "operationId": "getMyProfile",
        "tags": ["Membership"],
        "summary": "Get the authenticated user's profile",
        "description": "Returns the full profile of the currently authenticated user.",
        "security": [{ "sessionCookie": [] }],
        "responses": {
          "200": {
            "description": "User profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserProfile"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "503": {
            "description": "Server configuration error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateMyProfile",
        "tags": ["Membership"],
        "summary": "Update the authenticated user's profile",
        "description": "Updates one or more profile fields for the currently authenticated user. Only provided fields are changed.",
        "security": [{ "sessionCookie": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfileUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated profile",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserProfile"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Not authenticated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/memberships/directory": {
      "get": {
        "operationId": "getMemberDirectory",
        "tags": ["Membership"],
        "summary": "List public member directory",
        "description": "Returns members who have opted in to the public directory. No authentication required.",
        "responses": {
          "200": {
            "description": "Directory listing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DirectoryMember"
                      }
                    }
                  },
                  "required": ["members"]
                }
              }
            }
          }
        }
      }
    },
    "/v1/memberships/count": {
      "get": {
        "operationId": "getMemberCount",
        "tags": ["Membership"],
        "summary": "Get total member count",
        "description": "Returns the total number of non-suspended members. No authentication required.",
        "responses": {
          "200": {
            "description": "Member count",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "count": {
                      "type": "integer",
                      "minimum": 0,
                      "description": "Total number of non-suspended members"
                    }
                  },
                  "required": ["count"]
                },
                "example": { "count": 42 }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "post": {
        "operationId": "trackEvent",
        "tags": ["Telemetry"],
        "summary": "Track an analytics event",
        "description": "Records a telemetry event. Rate-limited to 100 events per minute per IP. Silently drops database errors to avoid failing client requests.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Event recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true }
                  },
                  "required": ["ok"]
                },
                "example": { "ok": true }
              }
            }
          },
          "400": {
            "description": "Validation error (missing or invalid event_name)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "429": {
            "description": "Rate limited",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/updates/latest": {
      "get": {
        "operationId": "getLatestUpdates",
        "tags": ["Updates"],
        "summary": "Get latest published project updates",
        "description": "Returns the most recent published project updates across all projects. No authentication required.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of updates to return (1-20, default 5)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Latest updates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProjectUpdate"
                      }
                    }
                  },
                  "required": ["updates"]
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{projectId}/updates": {
      "get": {
        "operationId": "listProjectUpdates",
        "tags": ["Updates"],
        "summary": "List updates for a project",
        "description": "Returns published updates for a specific project with pagination. No authentication required for reading published updates.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Project identifier",
            "schema": {
              "type": "string",
              "enum": ["stc", "lan", "peers", "everarchive"]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum number of updates to return (1-100, default 20)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of updates to skip (default 0)",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project updates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "updates": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProjectUpdate"
                      }
                    }
                  },
                  "required": ["updates"]
                }
              }
            }
          },
          "404": {
            "description": "Project not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProjectUpdate",
        "tags": ["Updates"],
        "summary": "Create a project update",
        "description": "Creates a new update for a project. Requires admin authentication.",
        "security": [{ "sessionCookie": [] }],
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Project identifier",
            "schema": {
              "type": "string",
              "enum": ["stc", "lan", "peers", "everarchive"]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Update created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectUpdate"
                }
              }
            }
          },
          "400": {
            "description": "Validation error (missing title)",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Not authenticated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Admin access required",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Project not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{projectId}/updates/{updateId}": {
      "get": {
        "operationId": "getProjectUpdate",
        "tags": ["Updates"],
        "summary": "Get a single project update",
        "description": "Returns a single project update by ID. No authentication required.",
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "description": "Project identifier",
            "schema": {
              "type": "string",
              "enum": ["stc", "lan", "peers", "everarchive"]
            }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "description": "Update UUID",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project update",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectUpdate"
                }
              }
            }
          },
          "404": {
            "description": "Update not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateProjectUpdate",
        "tags": ["Updates"],
        "summary": "Update a project update",
        "description": "Updates fields on an existing project update. Authors can edit their own; admins can edit any.",
        "security": [{ "sessionCookie": [] }],
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PatchUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated project update",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ProjectUpdate" }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "401": {
            "description": "Not authenticated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden — not the author and not admin",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Update not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteProjectUpdate",
        "tags": ["Updates"],
        "summary": "Delete a project update",
        "description": "Deletes a project update. Authors can delete their own; admins can delete any.",
        "security": [{ "sessionCookie": [] }],
        "parameters": [
          {
            "name": "projectId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          },
          {
            "name": "updateId",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Update deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "message": { "type": "string" }
                  },
                  "required": ["ok", "message"]
                },
                "example": { "ok": true, "message": "Update deleted" }
              }
            }
          },
          "401": {
            "description": "Not authenticated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "404": {
            "description": "Update not found",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          }
        }
      }
    },
    "/v1/admin/metrics": {
      "get": {
        "operationId": "getAdminMetrics",
        "tags": ["Admin"],
        "summary": "Get event metrics (admin only)",
        "description": "Returns aggregated telemetry event metrics. Requires admin session cookie.",
        "security": [{ "sessionCookie": [] }],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": false,
            "description": "ISO 8601 datetime — only include events after this time",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "description": "ISO 8601 datetime — only include events before this time",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Event metrics",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventMetrics"
                }
              }
            }
          },
          "401": {
            "description": "Not authenticated",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" }
              }
            }
          },
          "403": {
            "description": "Admin access required",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ErrorResponse" },
                "example": { "message": "Admin access required", "code": "FORBIDDEN" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "dc_session",
        "description": "Session cookie set by the /v1/auth/login endpoint. HttpOnly, Secure, SameSite=Lax."
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "description": "Standard error response returned by all endpoints on failure",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable error description"
          },
          "code": {
            "type": "string",
            "description": "Machine-readable error code",
            "examples": ["VALIDATION_ERROR", "NOT_FOUND", "INVALID_CREDENTIALS", "RATE_LIMITED", "FORBIDDEN", "CONFIG_ERROR", "EMAIL_EXISTS", "INVALID_AMOUNT", "INVALID_FREQUENCY", "AMOUNT_TOO_LARGE", "INVALID_RECOVERY_TOKEN"]
          }
        },
        "required": ["message", "code"]
      },
      "HealthResponse": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "timestamp": { "type": "string", "format": "date-time" },
          "runtime": { "type": "string" }
        },
        "required": ["ok", "timestamp", "runtime"]
      },
      "Address": {
        "type": "object",
        "properties": {
          "street": { "type": "string" },
          "city": { "type": "string" },
          "state": { "type": "string" },
          "zip": { "type": "string" },
          "country": { "type": "string" }
        },
        "required": ["street", "city", "state", "zip", "country"]
      },
      "Organization": {
        "type": "object",
        "description": "Organization metadata",
        "properties": {
          "legalName": { "type": "string", "examples": ["Distributed Creatives"] },
          "ein": { "type": "string", "examples": ["99-5135510"] },
          "taxStatus": { "type": "string", "examples": ["501(c)(3)"] },
          "address": { "$ref": "#/components/schemas/Address" },
          "yearEstablished": { "type": "integer", "examples": [2024] },
          "stateOfIncorporation": { "type": "string", "examples": ["Colorado"] },
          "contact": {
            "type": "object",
            "properties": {
              "general": { "type": "string", "format": "email" },
              "donor": { "type": "string", "format": "email" },
              "partnerships": { "type": "string", "format": "email" },
              "privacy": { "type": "string", "format": "email" }
            }
          }
        },
        "required": ["legalName", "ein", "taxStatus", "address", "yearEstablished", "stateOfIncorporation", "contact"]
      },
      "Project": {
        "type": "object",
        "description": "A Distributed Creatives project",
        "properties": {
          "id": {
            "type": "string",
            "description": "Project slug identifier",
            "examples": ["stc", "lan", "peers", "everarchive"]
          },
          "title": { "type": "string" },
          "type": {
            "type": "string",
            "description": "Project category",
            "examples": ["Advocacy Alliance", "Technology Platform", "Social Platform", "Preservation Infrastructure"]
          },
          "status": {
            "type": "string",
            "enum": ["Active", "Building", "Planned"]
          },
          "summary": { "type": "string" },
          "description": { "type": "string" },
          "currentFocus": { "type": "string" }
        },
        "required": ["id", "title", "type", "status", "summary", "description", "currentFocus"]
      },
      "TeamMember": {
        "type": "object",
        "description": "A public team member profile",
        "properties": {
          "id": { "type": "string", "description": "Slug identifier" },
          "name": { "type": "string" },
          "title": { "type": "string", "description": "Role / position title" },
          "summary": { "type": "string", "description": "Brief biography" }
        },
        "required": ["id", "name", "title", "summary"]
      },
      "DonateInfoResponse": {
        "type": "object",
        "description": "Donation information returned by GET /v1/donate",
        "properties": {
          "organization": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "ein": { "type": "string" },
              "tax_status": { "type": "string" },
              "tax_deductible": { "type": "boolean" },
              "platform": { "type": "string" },
              "donate_url_template": { "type": "string", "format": "uri" },
              "address": { "$ref": "#/components/schemas/Address" },
              "contact": {
                "type": "object",
                "properties": {
                  "general": { "type": "string", "format": "email" },
                  "donations": { "type": "string", "format": "email" }
                }
              },
              "website": { "type": "string", "format": "uri" },
              "description": { "type": "string" }
            }
          },
          "instructions": {
            "type": "object",
            "properties": {
              "description": { "type": "string" },
              "method": { "type": "string" },
              "content_type": { "type": "string" },
              "parameters": { "type": "object" },
              "example": { "type": "object" }
            }
          }
        },
        "required": ["organization", "instructions"]
      },
      "DonationRequest": {
        "type": "object",
        "description": "Request body for POST /v1/donate. All fields are optional — an empty body generates a donor-chosen-amount URL.",
        "properties": {
          "amount": {
            "type": "number",
            "description": "Donation amount in USD (positive, max 1,000,000). Omit for donor-chosen amount.",
            "exclusiveMinimum": 0,
            "maximum": 1000000
          },
          "frequency": {
            "type": "string",
            "description": "Donation frequency",
            "enum": ["one-time", "monthly"],
            "default": "one-time"
          }
        }
      },
      "DonationResponse": {
        "type": "object",
        "description": "Response from POST /v1/donate with the constructed checkout URL",
        "properties": {
          "donate_url": {
            "type": "string",
            "format": "uri",
            "description": "Every.org checkout URL"
          },
          "amount": {
            "type": ["number", "null"],
            "description": "Requested amount, or null if donor-chosen"
          },
          "frequency": {
            "type": "string",
            "enum": ["one-time", "monthly"]
          },
          "currency": {
            "type": "string",
            "const": "USD"
          },
          "tax_deductible": {
            "type": "boolean",
            "const": true
          },
          "organization": {
            "type": "object",
            "properties": {
              "name": { "type": "string" },
              "ein": { "type": "string" },
              "tax_status": { "type": "string" }
            }
          },
          "note": { "type": "string" }
        },
        "required": ["donate_url", "amount", "frequency", "currency", "tax_deductible", "organization", "note"]
      },
      "LoginRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "User's email address"
          },
          "password": {
            "type": "string",
            "description": "User's password"
          }
        },
        "required": ["email", "password"]
      },
      "LoginResponse": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": true },
          "user": {
            "$ref": "#/components/schemas/SessionUser"
          }
        },
        "required": ["ok", "user"]
      },
      "SessionUser": {
        "type": "object",
        "description": "User summary included in session/login responses",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fullName": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "membershipTier": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "active", "suspended"] },
          "bio": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "fullName", "email", "membershipTier", "status"]
      },
      "SessionResponse": {
        "type": "object",
        "description": "Session check response",
        "properties": {
          "authenticated": { "type": "boolean" },
          "user": {
            "$ref": "#/components/schemas/SessionUser"
          }
        },
        "required": ["authenticated"]
      },
      "RecoveryConfirmRequest": {
        "type": "object",
        "properties": {
          "token": {
            "type": "string",
            "description": "Recovery token from the password reset email"
          },
          "newPassword": {
            "type": "string",
            "minLength": 8,
            "description": "New password (minimum 8 characters)"
          }
        },
        "required": ["token", "newPassword"]
      },
      "SignupRequest": {
        "type": "object",
        "properties": {
          "fullName": {
            "type": "string",
            "description": "Full name of the new member"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address"
          },
          "password": {
            "type": "string",
            "minLength": 8,
            "description": "Password (minimum 8 characters)"
          },
          "membershipTier": {
            "type": "string",
            "description": "Membership tier. Currently only 'creator' is available. Legacy tier names are accepted and mapped to 'creator'.",
            "default": "creator",
            "enum": ["creator"]
          }
        },
        "required": ["fullName", "email", "password"]
      },
      "UserProfile": {
        "type": "object",
        "description": "Full user profile returned by /v1/memberships/me",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fullName": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "membershipTier": { "type": "string" },
          "status": { "type": "string", "enum": ["pending", "active", "suspended"] },
          "bio": { "type": "string" },
          "avatarUrl": { "type": "string" },
          "location": { "type": "string" },
          "website": { "type": "string" },
          "socialLinks": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Key-value pairs of social platform name to profile URL"
          },
          "directoryOptIn": { "type": "boolean" },
          "isAdmin": { "type": "boolean" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "fullName", "email", "membershipTier", "status"]
      },
      "ProfileUpdateRequest": {
        "type": "object",
        "description": "Fields to update on the user profile. Only provided fields are changed.",
        "properties": {
          "fullName": { "type": "string", "minLength": 1 },
          "bio": { "type": "string" },
          "membershipTier": {
            "type": "string",
            "enum": ["creator"]
          },
          "avatarUrl": {
            "type": "string",
            "maxLength": 2000,
            "description": "URL to avatar image"
          },
          "location": {
            "type": "string",
            "maxLength": 200
          },
          "website": {
            "type": "string",
            "maxLength": 2000
          },
          "socialLinks": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Key-value pairs of social platform name to profile URL"
          },
          "directoryOptIn": {
            "type": "boolean",
            "description": "Whether to appear in the public member directory"
          }
        },
        "minProperties": 1
      },
      "DirectoryMember": {
        "type": "object",
        "description": "Public directory entry for an opted-in member",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "fullName": { "type": "string" },
          "avatarUrl": { "type": "string" },
          "bio": { "type": "string" },
          "location": { "type": "string" }
        },
        "required": ["id", "fullName"]
      },
      "EventRequest": {
        "type": "object",
        "description": "Telemetry event to track",
        "properties": {
          "event_name": {
            "type": "string",
            "description": "Event type to record. Accepts the current frontend telemetry taxonomy plus legacy names kept for backward compatibility.",
            "enum": ["page_view", "form_submit", "donate_intent", "outbound_link", "cta_click", "signup_complete", "login_complete", "donate_click"]
          },
          "properties": {
            "type": "object",
            "additionalProperties": true,
            "description": "Arbitrary event properties (e.g., {\"path\": \"/donate\"})"
          },
          "user_id": {
            "type": ["string", "null"],
            "description": "Optional user ID"
          },
          "session_id": {
            "type": ["string", "null"],
            "description": "Optional session ID"
          }
        },
        "required": ["event_name"]
      },
      "ProjectUpdate": {
        "type": "object",
        "description": "A project update/news entry",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "projectId": { "type": "string" },
          "title": { "type": "string" },
          "bodyMarkdown": { "type": "string", "description": "Update body in Markdown" },
          "status": { "type": "string", "enum": ["draft", "published"] },
          "authorId": { "type": "string", "format": "uuid" },
          "publishedAt": { "type": ["string", "null"], "format": "date-time" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": ["string", "null"], "format": "date-time" }
        },
        "required": ["id", "projectId", "title", "bodyMarkdown", "status", "authorId", "createdAt"]
      },
      "CreateUpdateRequest": {
        "type": "object",
        "description": "Request body for creating a project update",
        "properties": {
          "title": {
            "type": "string",
            "minLength": 1,
            "description": "Update title"
          },
          "bodyMarkdown": {
            "type": "string",
            "description": "Update body in Markdown"
          },
          "status": {
            "type": "string",
            "enum": ["draft", "published"],
            "default": "draft",
            "description": "Publication status"
          }
        },
        "required": ["title"]
      },
      "PatchUpdateRequest": {
        "type": "object",
        "description": "Fields to update on a project update. Only provided fields are changed.",
        "properties": {
          "title": { "type": "string", "minLength": 1 },
          "bodyMarkdown": { "type": "string" },
          "status": { "type": "string", "enum": ["draft", "published"] }
        },
        "minProperties": 1
      },
      "EventMetrics": {
        "type": "object",
        "description": "Aggregated event metrics (admin only)",
        "properties": {
          "totalEvents": {
            "type": "integer",
            "minimum": 0
          },
          "eventCounts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "eventName": { "type": "string" },
                "count": { "type": "integer" }
              },
              "required": ["eventName", "count"]
            }
          },
          "topPages": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "page": { "type": "string" },
                "views": { "type": "integer" }
              },
              "required": ["page", "views"]
            }
          }
        },
        "required": ["totalEvents", "eventCounts", "topPages"]
      }
    }
  }
}
