openapi: 3.1.0
info:
  title: schematize API
  version: 0.1.0
  summary: Store, version, replicate, and publish Claude Code skills — plus the publisher and admin/backoffice workflow.
  description: |
    The **schematize platform API** (`api.schematize.org`) is the single backend behind the
    schematize app, the skills catalog, and the `backoffice.schematize.org` admin console.

    ## What it does
    - **Store & version your skills.** Any authenticated user can upload a skill package,
      version it (SemVer), and keep it **private**. Nothing is public until it is reviewed.
    - **Replicate your environment.** Logged into the same account, the app's agent can pull a
      manifest of all your skills and download them to reconstruct `~/.claude/skills`.
    - **Publish for everyone — behind two gates.** To publish at all you must hold a
      **publisher** account (request it; an admin approves it). Then every publish *and every
      update* of a skill goes through **manual review** before it reaches the public catalog.
    - **Administer everything (backoffice).** An admin manages users, the publisher-request
      queue, the publication **review queue**, audits every skill (including private ones) with
      full version history, and tracks publications across the adjacent sites (e.g. blueprint).

    ## Roles (authorization is decided server-side, deny-by-default)
    - `user` — upload/store/version **own** skills (private), download them, replicate the
      environment, request a publisher account.
    - `publisher` — a user approved to *submit* skills for publication. Each submission still
      goes through review.
    - `admin` — the backoffice: users, publisher requests, review queue, audit, publications.

    Ownership maps to the IdP's ReBAC (`object_type: skill`, `relation: owner|publisher`);
    platform roles to `platform:publisher` / `platform:admin`. The API enforces these on the
    server — a token never carries a role list (it stays thin: `sub` + `aal`).

    ## Skill lifecycle
    ```
    upload (private) ──▶ versions (SemVer, checksum, changelog)
                          └─[to publish]─▶ become a publisher ──▶ submit a version
                                            └─▶ review queue ──▶ admin approve ──▶ PUBLIC (catalog)
                                                              └─▶ admin reject  ──▶ back to author
    an update to a published skill = a new version ──▶ back through the review queue.
    ```

    ## Authentication (for the app's agent)
    This API is a **resource server**. It validates a JWT access token issued by the house IdP
    at `auth.schematize.org` (RS256, verified via JWKS; asymmetric algorithms only — `none`/`HS*`
    are rejected). Discovery: `https://auth.schematize.org/.well-known/openid-configuration`.
    A CLI/agent obtains a token via the **OAuth 2.0 Device Authorization grant**
    (`POST /device_authorization` at the IdP) — no browser redirect, no client secret.

    ## Non-negotiable floors (encoded in this contract)
    - Uploads are **untrusted content**: bounded size (**413** over the limit), validated
      structure and no path traversal in the archive (**422** on a bad package).
    - **Nothing is published without a manual review** (`POST /admin/review-queue/{id}/approve`).
    - **Deny-by-default:** `401` (no/invalid token), `403` (not owner / not publisher / not admin).
    - Every admin action and state transition is written to the **audit log**.
  contact:
    name: schematize
    url: https://schematize.org
  license:
    name: Proprietary
servers:
  - url: https://api.schematize.org
    description: Production
security:
  - bearerAuth: []
tags:
  - name: System
    description: Health and readiness.
  - name: Catalog
    description: The public, published skills catalog (read-only, no auth).
  - name: My Skills
    description: Upload, version, and download your own skills (private by default).
  - name: Environment
    description: Replicate your `~/.claude/skills` on another machine.
  - name: Publisher
    description: Become a publisher and submit skills for publication.
  - name: Social
    description: Ratings, reviews, comments, profiles, follows, feed, notifications.
  - name: Admin · Users
    description: Backoffice — user administration (admin only).
  - name: Admin · Publisher Requests
    description: Backoffice — approve/reject publisher accounts (admin only).
  - name: Admin · Review Queue
    description: Backoffice — manual review of skill publications and updates (admin only).
  - name: Admin · Skills
    description: Backoffice — audit every skill and version, including private ones (admin only).
  - name: Admin · Publications
    description: Backoffice — track/manage publications on adjacent sites, e.g. blueprint (admin only).
  - name: Admin · Audit
    description: Backoffice — the audit log of every admin action and state transition (admin only).

paths:
  /health:
    get:
      tags: [System]
      summary: Liveness
      security: []
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Health" } } } }
  /ready:
    get:
      tags: [System]
      summary: Readiness (DB reachable)
      security: []
      responses:
        "200": { description: Ready }
        "503": { description: Not ready }

  /skills:
    get:
      tags: [Catalog]
      summary: List published skills
      security: []
      parameters:
        - { name: sort, in: query, schema: { type: string, enum: [name, top, new], default: name } }
        - { name: category, in: query, schema: { type: string } }
        - { name: q, in: query, schema: { type: string }, description: Full-text query }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SkillPage" } } } }
  /skills/top:
    get:
      tags: [Catalog]
      summary: Top-rated published skills
      security: []
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SkillPage" } } } }
  /skills/{slug}:
    get:
      tags: [Catalog]
      summary: Get a published skill
      security: []
      parameters: [ { $ref: "#/components/parameters/Slug" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Skill" } } } }
        "404": { $ref: "#/components/responses/NotFound" }
  /skills/{slug}/versions:
    get:
      tags: [Catalog]
      summary: List a published skill's versions
      security: []
      parameters: [ { $ref: "#/components/parameters/Slug" } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/SkillVersion" } } } } }
        "404": { $ref: "#/components/responses/NotFound" }
  /skills/{slug}/download:
    get:
      tags: [Catalog]
      summary: Download a published skill package
      security: []
      parameters:
        - { $ref: "#/components/parameters/Slug" }
        - { name: version, in: query, schema: { type: string }, description: SemVer; defaults to latest }
      responses:
        "302": { description: Redirect to the versioned package (`.zip`) }
        "404": { $ref: "#/components/responses/NotFound" }
  /versions:
    get:
      tags: [Catalog]
      summary: Map of slug → latest published version
      security: []
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, additionalProperties: { type: string } } } } }

  /me/skills:
    get:
      tags: [My Skills]
      summary: List my skills (all visibilities)
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SkillPage" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [My Skills]
      summary: Register a new skill (owner = you, private)
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: "#/components/schemas/SkillCreate" } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Skill" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Unprocessable" }
  /me/skills/{slug}:
    parameters: [ { $ref: "#/components/parameters/Slug" } ]
    get:
      tags: [My Skills]
      summary: Get one of my skills
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Skill" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [My Skills]
      summary: Update my skill's metadata
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/SkillUpdate" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Skill" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    delete:
      tags: [My Skills]
      summary: Delete my skill (only if never published)
      responses:
        "204": { description: Deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
  /me/skills/{slug}/versions:
    parameters: [ { $ref: "#/components/parameters/Slug" } ]
    get:
      tags: [My Skills]
      summary: List my skill's versions
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/SkillVersion" } } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [My Skills]
      summary: Upload a new version (the skill package)
      description: |
        Upload the packaged skill as a `.zip`/`.tar.gz` (the same layout the CLI installs:
        `SKILL.md` + `references/`, `commands/`, `assets/`…). The server validates the archive
        (size, structure, no path traversal), computes a SHA-256 checksum and stores it as an
        immutable version. Visibility stays **private** until submitted and approved.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [package, version]
              properties:
                package: { type: string, format: binary, description: The skill archive (.zip/.tar.gz) }
                version: { type: string, description: SemVer, e.g. 1.2.0 }
                changelog: { type: string }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/SkillVersion" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/TooLarge" }
        "422": { $ref: "#/components/responses/Unprocessable" }
  /me/skills/{slug}/versions/{version}:
    parameters:
      - { $ref: "#/components/parameters/Slug" }
      - { $ref: "#/components/parameters/Version" }
    get:
      tags: [My Skills]
      summary: Get one of my skill versions
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SkillVersion" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /me/skills/{slug}/versions/{version}/download:
    parameters:
      - { $ref: "#/components/parameters/Slug" }
      - { $ref: "#/components/parameters/Version" }
    get:
      tags: [My Skills]
      summary: Download my skill version's package
      responses:
        "302": { description: Redirect to the package }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }

  /me/environment:
    get:
      tags: [Environment]
      summary: Manifest of my skills (to replicate the environment)
      description: |
        Returns every skill you own with its latest version, checksum and download URL — enough
        for the app's agent to rebuild `~/.claude/skills` on another machine, logged into the
        same account.
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/EnvironmentManifest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /me/environment/download:
    get:
      tags: [Environment]
      summary: Download my whole environment as one bundle
      responses:
        "200": { description: A `.tar.gz` bundle of all your skills, content: { application/gzip: { schema: { type: string, format: binary } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /me/publisher-request:
    get:
      tags: [Publisher]
      summary: My publisher-account request status
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/PublisherRequest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { description: You have no request on file }
    post:
      tags: [Publisher]
      summary: Request a publisher account
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PublisherRequestCreate" } } } }
      responses:
        "201": { description: Submitted, content: { application/json: { schema: { $ref: "#/components/schemas/PublisherRequest" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "409": { description: A request is already pending or you are already a publisher }
  /me/skills/{slug}/versions/{version}/submit:
    parameters:
      - { $ref: "#/components/parameters/Slug" }
      - { $ref: "#/components/parameters/Version" }
    post:
      tags: [Publisher]
      summary: Submit a version for publication (publisher only)
      description: Requires the `publisher` role. Creates a submission in the review queue; the skill/version is **not** public until an admin approves it.
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/SubmissionCreate" } } } }
      responses:
        "201": { description: Submitted for review, content: { application/json: { schema: { $ref: "#/components/schemas/Submission" } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { description: Not a publisher, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
        "409": { description: Already submitted / already published }
  /me/submissions:
    get:
      tags: [Publisher]
      summary: My publication submissions
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Submission" } } } } }
        "401": { $ref: "#/components/responses/Unauthorized" }
  /me/submissions/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    delete:
      tags: [Publisher]
      summary: Withdraw a submission (while still pending)
      responses:
        "204": { description: Withdrawn }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { description: Already decided }

  /skills/{slug}/ratings:
    parameters: [ { $ref: "#/components/parameters/Slug" } ]
    get: { tags: [Social], summary: List ratings, security: [], responses: { "200": { description: OK } } }
    put:
      tags: [Social]
      summary: Set my rating (1–5)
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/RatingInput" } } } }
      responses: { "200": { description: OK }, "401": { $ref: "#/components/responses/Unauthorized" } }
    delete: { tags: [Social], summary: Clear my rating, responses: { "204": { description: Cleared }, "401": { $ref: "#/components/responses/Unauthorized" } } }
  /skills/{slug}/reviews:
    parameters: [ { $ref: "#/components/parameters/Slug" } ]
    get: { tags: [Social], summary: List reviews, security: [], responses: { "200": { description: OK } } }
    post:
      tags: [Social]
      summary: Write/replace my review
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ReviewInput" } } } }
      responses: { "201": { description: Created }, "401": { $ref: "#/components/responses/Unauthorized" } }
  /skills/{slug}/comments:
    parameters: [ { $ref: "#/components/parameters/Slug" } ]
    get: { tags: [Social], summary: List comments (threaded), security: [], responses: { "200": { description: OK } } }
    post:
      tags: [Social]
      summary: Post a comment
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/CommentInput" } } } }
      responses: { "201": { description: Created }, "401": { $ref: "#/components/responses/Unauthorized" } }
  /profiles/{handle}:
    parameters: [ { name: handle, in: path, required: true, schema: { type: string } } ]
    get: { tags: [Social], summary: Get a public profile, security: [], responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Profile" } } } }, "404": { $ref: "#/components/responses/NotFound" } } }
  /me/profile:
    put:
      tags: [Social]
      summary: Update my profile
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ProfileInput" } } } }
      responses: { "200": { description: OK }, "401": { $ref: "#/components/responses/Unauthorized" } }
    delete:
      tags: [Social]
      summary: Delete my profile (step-up AAL required)
      responses: { "204": { description: Deleted }, "401": { $ref: "#/components/responses/Unauthorized" }, "403": { description: Step-up authentication required } }
  /me/feed:
    get: { tags: [Social], summary: My activity feed, responses: { "200": { description: OK }, "401": { $ref: "#/components/responses/Unauthorized" } } }
  /me/notifications:
    get: { tags: [Social], summary: My notifications, responses: { "200": { description: OK }, "401": { $ref: "#/components/responses/Unauthorized" } } }
  /reports:
    post:
      tags: [Social]
      summary: Report content
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/ReportInput" } } } }
      responses: { "201": { description: Filed }, "401": { $ref: "#/components/responses/Unauthorized" } }

  /admin/users:
    get:
      tags: [Admin · Users]
      summary: List users
      parameters:
        - { name: role, in: query, schema: { $ref: "#/components/schemas/Role" } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/UserStatus" } }
        - { name: q, in: query, schema: { type: string } }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/UserPage" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/users/{sub}:
    parameters: [ { name: sub, in: path, required: true, schema: { type: string }, description: The user's opaque identity (ULID) } ]
    get:
      tags: [Admin · Users]
      summary: Get a user
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/User" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Admin · Users]
      summary: Update a user (status / roles)
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/AdminUserUpdate" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/User" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }

  /admin/publisher-requests:
    get:
      tags: [Admin · Publisher Requests]
      summary: The publisher-request queue
      parameters: [ { name: status, in: query, schema: { $ref: "#/components/schemas/PublisherRequestStatus" } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/PublisherRequest" } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/publisher-requests/{id}/approve:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Admin · Publisher Requests]
      summary: Approve — grant the publisher role
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/PublisherRequest" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/publisher-requests/{id}/reject:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Admin · Publisher Requests]
      summary: Reject — with a reason
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/Decision" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/PublisherRequest" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }

  /admin/review-queue:
    get:
      tags: [Admin · Review Queue]
      summary: Pending publication submissions
      parameters: [ { name: status, in: query, schema: { $ref: "#/components/schemas/SubmissionStatus" } } ]
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Submission" } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/review-queue/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Admin · Review Queue]
      summary: Full submission (skill, version, package, diff to the live version)
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SubmissionDetail" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
  /admin/review-queue/{id}/claim:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Admin · Review Queue]
      summary: Claim for review (→ in_review)
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Submission" } } } }, "403": { $ref: "#/components/responses/Forbidden" } }
  /admin/review-queue/{id}/approve:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Admin · Review Queue]
      summary: Approve — publish the version to the public catalog
      requestBody: { content: { application/json: { schema: { $ref: "#/components/schemas/Decision" } } } }
      responses:
        "200": { description: Published, content: { application/json: { schema: { $ref: "#/components/schemas/Submission" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { description: Not in a reviewable state }
  /admin/review-queue/{id}/reject:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    post:
      tags: [Admin · Review Queue]
      summary: Reject — with notes, back to the author
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/Decision" } } } }
      responses:
        "200": { description: Rejected, content: { application/json: { schema: { $ref: "#/components/schemas/Submission" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }

  /admin/skills:
    get:
      tags: [Admin · Skills]
      summary: Every skill (including private) — audit
      parameters:
        - { name: visibility, in: query, schema: { $ref: "#/components/schemas/Visibility" } }
        - { name: owner, in: query, schema: { type: string }, description: Owner handle or sub }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SkillPage" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/skills/{slug}:
    parameters: [ { $ref: "#/components/parameters/Slug" } ]
    get:
      tags: [Admin · Skills]
      summary: Full skill with owner and every version (audit)
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/SkillAdmin" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Admin · Skills]
      summary: Upsert catalog metadata (used by the sync/generator)
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/SkillUpdate" } } } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Skill" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/skills/{slug}/versions/{version}/download:
    parameters:
      - { $ref: "#/components/parameters/Slug" }
      - { $ref: "#/components/parameters/Version" }
    get:
      tags: [Admin · Skills]
      summary: Download any version's package (audit)
      responses: { "302": { description: Redirect to the package }, "403": { $ref: "#/components/responses/Forbidden" } }

  /admin/publications:
    get:
      tags: [Admin · Publications]
      summary: Publications across adjacent sites (blueprint, catalog, …)
      parameters:
        - { name: site, in: query, schema: { $ref: "#/components/schemas/Site" } }
        - { name: status, in: query, schema: { $ref: "#/components/schemas/PublicationStatus" } }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/Publication" } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
    post:
      tags: [Admin · Publications]
      summary: Register/create a publication
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PublicationCreate" } } } }
      responses:
        "201": { description: Created, content: { application/json: { schema: { $ref: "#/components/schemas/Publication" } } } }
        "403": { $ref: "#/components/responses/Forbidden" }
  /admin/publications/{id}:
    parameters: [ { $ref: "#/components/parameters/Id" } ]
    get:
      tags: [Admin · Publications]
      summary: Get a publication
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Publication" } } } }, "403": { $ref: "#/components/responses/Forbidden" }, "404": { $ref: "#/components/responses/NotFound" } }
    patch:
      tags: [Admin · Publications]
      summary: Update a publication (status / version)
      requestBody: { required: true, content: { application/json: { schema: { $ref: "#/components/schemas/PublicationUpdate" } } } }
      responses: { "200": { description: OK, content: { application/json: { schema: { $ref: "#/components/schemas/Publication" } } } }, "403": { $ref: "#/components/responses/Forbidden" } }
    delete:
      tags: [Admin · Publications]
      summary: Unregister a publication
      responses: { "204": { description: Deleted }, "403": { $ref: "#/components/responses/Forbidden" } }

  /admin/audit-log:
    get:
      tags: [Admin · Audit]
      summary: The audit log (every admin action and state transition)
      parameters:
        - { name: actor, in: query, schema: { type: string } }
        - { name: action, in: query, schema: { type: string } }
        - { name: target_type, in: query, schema: { type: string } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: array, items: { $ref: "#/components/schemas/AuditEntry" } } } } }
        "403": { $ref: "#/components/responses/Forbidden" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        A JWT access token from the house IdP (`auth.schematize.org`), RS256, verified via JWKS
        (asymmetric only — `none`/`HS*` rejected). Discovery:
        `https://auth.schematize.org/.well-known/openid-configuration`. The app's agent obtains
        one via the **OAuth 2.0 Device Authorization grant** (`POST /device_authorization`).
    oidc:
      type: openIdConnect
      openIdConnectUrl: https://auth.schematize.org/.well-known/openid-configuration
  parameters:
    Slug: { name: slug, in: path, required: true, schema: { type: string, pattern: "^[a-z0-9][a-z0-9-]*$" }, description: Skill slug }
    Version: { name: version, in: path, required: true, schema: { type: string }, description: SemVer }
    Id: { name: id, in: path, required: true, schema: { type: string }, description: Resource id (ULID) }
    Page: { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
    Limit: { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 24 } }
  responses:
    Unauthorized: { description: Missing or invalid token, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Forbidden: { description: Not allowed (not owner / not publisher / not admin), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    NotFound: { description: Not found, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Conflict: { description: Conflict (already exists / wrong state), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    TooLarge: { description: Package exceeds the size limit, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    Unprocessable: { description: Invalid package or input (bad structure, path traversal, bad SemVer), content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
    RateLimited: { description: Too many requests, content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } } }
  schemas:
    Health: { type: object, properties: { status: { type: string, example: ok }, version: { type: string } } }
    Error:
      type: object
      required: [error]
      properties:
        error: { type: string, description: Stable machine code, e.g. forbidden, not_found, package_too_large }
        message: { type: string }
        details: { type: object, additionalProperties: true }
    Role: { type: string, enum: [user, publisher, admin] }
    UserStatus: { type: string, enum: [active, suspended] }
    Visibility: { type: string, enum: [private, submitted, published], description: A skill is private until submitted; public once a version is approved }
    VersionStatus: { type: string, enum: [draft, submitted, in_review, published, rejected] }
    SubmissionStatus: { type: string, enum: [submitted, in_review, approved, rejected, withdrawn] }
    PublisherRequestStatus: { type: string, enum: [pending, approved, rejected] }
    PublicationStatus: { type: string, enum: [draft, published, unpublished] }
    Site: { type: string, enum: [blueprint, skills, org], description: Adjacent site a publication lives on }
    Skill:
      type: object
      properties:
        slug: { type: string }
        name: { type: string }
        summary: { type: string }
        description: { type: string }
        category: { type: string }
        visibility: { $ref: "#/components/schemas/Visibility" }
        owner: { $ref: "#/components/schemas/OwnerRef" }
        latest_version: { type: string, nullable: true }
        verified: { type: boolean }
        sponsor_name: { type: string, nullable: true }
        sponsor_url: { type: string, nullable: true }
        rating_avg: { type: number, nullable: true }
        rating_count: { type: integer }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    SkillAdmin:
      allOf:
        - $ref: "#/components/schemas/Skill"
        - type: object
          properties:
            versions: { type: array, items: { $ref: "#/components/schemas/SkillVersion" } }
    OwnerRef:
      type: object
      properties:
        sub: { type: string }
        handle: { type: string, nullable: true }
        display_name: { type: string, nullable: true }
    SkillCreate:
      type: object
      required: [slug, name]
      properties:
        slug: { type: string, pattern: "^[a-z0-9][a-z0-9-]*$" }
        name: { type: string }
        summary: { type: string }
        description: { type: string }
        category: { type: string }
    SkillUpdate:
      type: object
      properties:
        name: { type: string }
        summary: { type: string }
        description: { type: string }
        category: { type: string }
        verified: { type: boolean }
        sponsor_name: { type: string }
        sponsor_url: { type: string }
    SkillVersion:
      type: object
      properties:
        skill_slug: { type: string }
        version: { type: string }
        status: { $ref: "#/components/schemas/VersionStatus" }
        changelog: { type: string, nullable: true }
        checksum_sha256: { type: string }
        size_bytes: { type: integer }
        package_url: { type: string, nullable: true, description: Present when you may download it }
        created_at: { type: string, format: date-time }
    SkillPage:
      type: object
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/Skill" } }
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
    EnvironmentManifest:
      type: object
      properties:
        generated_at: { type: string, format: date-time }
        skills:
          type: array
          items:
            type: object
            properties:
              slug: { type: string }
              version: { type: string }
              checksum_sha256: { type: string }
              download_url: { type: string }
    PublisherRequest:
      type: object
      properties:
        id: { type: string }
        user: { $ref: "#/components/schemas/OwnerRef" }
        status: { $ref: "#/components/schemas/PublisherRequestStatus" }
        justification: { type: string }
        reviewed_by: { type: string, nullable: true }
        reviewed_at: { type: string, format: date-time, nullable: true }
        reason: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
    PublisherRequestCreate:
      type: object
      required: [justification]
      properties:
        justification: { type: string, description: Who you are and what you intend to publish }
        links: { type: array, items: { type: string }, description: GitHub, site, etc. }
    Submission:
      type: object
      properties:
        id: { type: string }
        skill_slug: { type: string }
        version: { type: string }
        submitted_by: { $ref: "#/components/schemas/OwnerRef" }
        status: { $ref: "#/components/schemas/SubmissionStatus" }
        reviewer: { type: string, nullable: true }
        notes: { type: string, nullable: true }
        submitted_at: { type: string, format: date-time }
        decided_at: { type: string, format: date-time, nullable: true }
    SubmissionDetail:
      allOf:
        - $ref: "#/components/schemas/Submission"
        - type: object
          properties:
            skill: { $ref: "#/components/schemas/Skill" }
            skill_version: { $ref: "#/components/schemas/SkillVersion" }
            previous_version: { type: string, nullable: true, description: The currently-published version, for diffing }
            package_url: { type: string }
    SubmissionCreate:
      type: object
      properties:
        note: { type: string, description: Optional note to the reviewer }
    Decision:
      type: object
      properties:
        notes: { type: string }
        reason: { type: string }
    User:
      type: object
      properties:
        sub: { type: string }
        handle: { type: string, nullable: true }
        display_name: { type: string, nullable: true }
        roles: { type: array, items: { $ref: "#/components/schemas/Role" } }
        status: { $ref: "#/components/schemas/UserStatus" }
        skills_count: { type: integer }
        created_at: { type: string, format: date-time }
    UserPage:
      type: object
      properties:
        items: { type: array, items: { $ref: "#/components/schemas/User" } }
        page: { type: integer }
        limit: { type: integer }
        total: { type: integer }
    AdminUserUpdate:
      type: object
      properties:
        status: { $ref: "#/components/schemas/UserStatus" }
        roles: { type: array, items: { $ref: "#/components/schemas/Role" } }
    Publication:
      type: object
      properties:
        id: { type: string }
        site: { $ref: "#/components/schemas/Site" }
        slug: { type: string }
        title: { type: string }
        status: { $ref: "#/components/schemas/PublicationStatus" }
        version: { type: string, nullable: true }
        url: { type: string, nullable: true }
        published_at: { type: string, format: date-time, nullable: true }
        updated_at: { type: string, format: date-time }
    PublicationCreate:
      type: object
      required: [site, slug, title]
      properties:
        site: { $ref: "#/components/schemas/Site" }
        slug: { type: string }
        title: { type: string }
        status: { $ref: "#/components/schemas/PublicationStatus" }
    PublicationUpdate:
      type: object
      properties:
        title: { type: string }
        status: { $ref: "#/components/schemas/PublicationStatus" }
        version: { type: string }
    AuditEntry:
      type: object
      properties:
        id: { type: string }
        actor_sub: { type: string }
        action: { type: string, description: e.g. publisher_request.approve, review.approve, user.suspend }
        target_type: { type: string }
        target_id: { type: string }
        metadata: { type: object, additionalProperties: true }
        created_at: { type: string, format: date-time }
    Profile:
      type: object
      properties:
        handle: { type: string }
        display_name: { type: string, nullable: true }
        bio: { type: string, nullable: true }
        avatar_url: { type: string, nullable: true }
    ProfileInput:
      type: object
      properties: { handle: { type: string }, display_name: { type: string }, bio: { type: string }, avatar_url: { type: string } }
    RatingInput: { type: object, required: [stars], properties: { stars: { type: integer, minimum: 1, maximum: 5 } } }
    ReviewInput: { type: object, required: [stars], properties: { stars: { type: integer, minimum: 1, maximum: 5 }, title: { type: string }, body: { type: string } } }
    CommentInput: { type: object, required: [body], properties: { body: { type: string }, parent_id: { type: string, nullable: true } } }
    ReportInput: { type: object, required: [target_type, target_id, reason], properties: { target_type: { type: string }, target_id: { type: string }, reason: { type: string } } }
